From ab4520683ab325046f2a9fe6ebf127dbbab60dfe Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 19 Nov 2014 17:19:52 +0100 Subject: [PATCH 001/158] Added readthedocs badge --- README.rst | 9 ++++++--- gitdb/ext/smmap | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 194e246..186218d 100644 --- a/README.rst +++ b/README.rst @@ -12,6 +12,9 @@ Installation .. image:: https://pypip.in/py_versions/gitdb/badge.svg :target: https://pypi.python.org/pypi/gitdb/ :alt: Supported Python versions +.. image:: https://readthedocs.org/projects/gitdb/badge/?version=latest + :target: https://readthedocs.org/projects/gitdb/?badge=latest + :alt: Documentation Status From `PyPI `_ @@ -44,7 +47,7 @@ DEVELOPMENT :target: https://travis-ci.org/gitpython-developers/gitdb .. image:: https://coveralls.io/repos/gitpython-developers/gitdb/badge.png - :target: https://coveralls.io/r/gitpython-developers/gitdb + :target: https://coveralls.io/r/gitpython-developers/gitdb The library is considered mature, and not under active development. It's primary (known) use is in git-python. @@ -52,10 +55,10 @@ INFRASTRUCTURE ============== * Mailing List - * http://groups.google.com/group/git-python + * http://groups.google.com/group/git-python * Issue Tracker - * https://github.com/gitpython-developers/gitdb/issues + * https://github.com/gitpython-developers/gitdb/issues LICENSE ======= diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 28fd45e..eb40b44 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 28fd45e0a7018f166820a5e00fce2ccb05ebdb61 +Subproject commit eb40b44ce4a6e646aabf7b7091d876738336c42f From 5e3dbccb05f85f178d3260a27df2f59a69221668 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Wed, 17 Dec 2014 23:09:42 -0500 Subject: [PATCH 002/158] Bring gitdb.test back into distribution --- MANIFEST.in | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index b14aed9..597944f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,7 +8,7 @@ include gitdb/_fun.c include gitdb/_delta_apply.c include gitdb/_delta_apply.h -prune gitdb/test +graft gitdb/test global-exclude .git* global-exclude *.pyc diff --git a/setup.py b/setup.py index dc142c5..1d327eb 100755 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ def get_data_files(self): author = __author__, author_email = __contact__, url = __homepage__, - packages = ('gitdb', 'gitdb.db', 'gitdb.utils'), + packages = ('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), package_dir = {'gitdb':'gitdb'}, ext_modules=[Extension('gitdb._perf', ['gitdb/_fun.c', 'gitdb/_delta_apply.c'], include_dirs=['gitdb'])], license = "BSD License", From c38bd19706abe5cf0bf0e7b3e9ad2b3e554d28ef Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Jan 2015 13:47:19 +0100 Subject: [PATCH 003/158] Increased initial size of decompressed data to obtain loose object header information This appears to fix https://github.com/gitpython-developers/GitPython/issues/220 , in this particular case. Nonetheless, we might just have gotten lucky here, and the actual issue is not yet solved and can thus re-occour. It would certainly be best to churn through plenty of loose objects to assure this truly works now. Maybe the pack could be recompressed as loose objects to get a sufficiently large data set --- gitdb/stream.py | 7 +++++-- .../88/8401851f15db0eed60eb1bc29dec5ddcace911 | Bin 0 -> 222336 bytes gitdb/test/performance/test_pack.py | 3 ++- gitdb/test/test_stream.py | 13 ++++++++----- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 gitdb/test/fixtures/objects/88/8401851f15db0eed60eb1bc29dec5ddcace911 diff --git a/gitdb/stream.py b/gitdb/stream.py index edd6dd2..b0a8900 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -100,7 +100,9 @@ def _parse_header_info(self): :return: parsed type_string, size""" # read header - maxb = 512 # should really be enough, cgit uses 8192 I believe + # should really be enough, cgit uses 8192 I believe + # And for good reason !! This needs to be that high for the header to be read correctly in all cases + maxb = 8192 self._s = maxb hdr = self.read(maxb) hdrend = hdr.find(NULL_BYTE) @@ -243,7 +245,7 @@ def read(self, size=-1): # moving the window into the memory map along as we decompress, which keeps # the tail smaller than our chunk-size. This causes 'only' the chunk to be # copied once, and another copy of a part of it when it creates the unconsumed - # tail. We have to use it to hand in the appropriate amount of bytes durin g + # tail. We have to use it to hand in the appropriate amount of bytes during # the next read. tail = self._zip.unconsumed_tail if tail: @@ -284,6 +286,7 @@ def read(self, size=-1): else: unused_datalen = len(self._zip.unconsumed_tail) + len(self._zip.unused_data) # end handle very special case ... + self._cbr += len(indata) - unused_datalen self._br += len(dcompdat) diff --git a/gitdb/test/fixtures/objects/88/8401851f15db0eed60eb1bc29dec5ddcace911 b/gitdb/test/fixtures/objects/88/8401851f15db0eed60eb1bc29dec5ddcace911 new file mode 100644 index 0000000000000000000000000000000000000000..d60aeeff74d2983bcc2abd03163d2cc8c422f819 GIT binary patch literal 222336 zcmV(zK<2-A0gReca4t*`reoW-ZQHhO<0ScF+qO<@+qP{xIkBDWzq;OiYieq`rn)cY zrl0C}Oza#@h*(%ySedzii2012O|1lA%*;)!jUDJstR0Me6?kAY$rk__!v%mWvr>c|!t=BR(^2i-&|PE)rDF zfk!^fRVBjq@wl-S!7GHO6AF3#{Ejj{^|X?hEje2mmunc@?hfXE+wMbLFsSn}W!6+7 zrYYW(|IjgGY6HhALjLvWd9t14+jh1M1wY}9KMQ@OOYlBu(6qS&Rz zG;LAnQ9Rj@lNGbDc1h^FSj3=9lCpHf@k2234o%CG%}5xx#lpPiD%5;uG3Xu2Yr(BE zon1Lm&JI?IPN%}Rb~b9`J+dXv>-8lU_vK#mm(pQQ7@{r{q2nMeA~qv@Gw_PqX;EKR zYJva*$1;8Hs{v+8yqVOgCAu|<0k-zn#_`2qrpm?Y)|1sb9eYEQUIOo7(7eT7IW>rD z?E~Qg7s|}D(Xf6qCW&m3kjSFF{$`C%`>Mr%1$O^_v}k3|KGQ{)D8SLPAkOhWPZ`cC zTr<&Dd2M?Xsd*_}V)+fm|3y>!`!?`BmM%X#f18>Fv0%gV(M0m5FYVjc^TBhVZhN>B zu4pO}qYsQ01JyGW&j?Y^0~ts%-Lbxx#!VtmBwzj1&3VWV+GKHGO(qNvzo#vR{&FYp3$w zc4GpwFu}I_h6%VBt7ja9Tfabd+ShP7$JuqZ4NNSYs=uu z8wE?Q0BA=a`2`jP95%&u>kb;CbZ1m4k@}HhSjcNEx3x!DVY}MHHLf!|bJZ+Em$9b~ za&;zCbTC$oV9E|m>Db+qog2;$-Bc#Zk5GW8B!-f~&+h2pA0L5N?ap_KsrSmh5LCus zCy`swgR^0vIWd*ul+cD(_e5zPdfG)xJJ1-k0`oygR|SN+^@J7@q_VR2Zz$AF>TcG1 zjg&v}aMCNNokyrbrU61s$f0t##PrL(?V`#ZZECh)u2>uZ^BN6doe+HpOO*$L#od20 z*^|GFj{XSQugs$!^!hZ#Ei#Y}Y|CfljmV~fgs6_L_=0xNY&btn3wP;sUJ61y&{5X$ z&tMgRGaF_77+>&!0GUI#G?SYC!wqn$p2-XzDA1k!dq)a>jgj!*2gA@%<7$#qnE$5HR4@*Evg7r zkckvjr^Kk@L}OWM&w^2fn*4Wj_B&+qFl`2fIX?VXcNltJq&X9*LwxMorn43jK}QBZ zSq@8!6<~+-%Q|Ni-7(!Wy?w32Q;A5C2BN*ntp*mcU0%DGLrmZ?f^HVXle;tEtj;J$ zH;rDQG*EySc(1En46dl$@+Sp^V&=StZtZ!WNYvg#|`S9#W5sVWE((c zcH3nuNGKume`Uz5gYrv)7;X!?f7h4+wIsk5xfVHM$7eY-hS`oJukZG z(SP#jJ^tYN{p%IuZuhqA!8iHIj|eueTaw^53?dVZ+*Q4Yq&!P`HA{2Y{zrQiNkd)S zUv^UHM*_q9;qDXmcocr41!2Py`O|BVuqH>)aHXsAi@k+OyK zBMsWy(Ny?X*J;T8;y2g7>E<{2hq392{kJT0(&E>@>&G7oh%8g@^WgJ}_&tU1>Z_Ie z_0J*M#~+b^*YRbk{kk`}-l~J1vtB}P(WS&z+tuRgiGnP$@XZuF8fIs8!X(j9$sd8Z z*bEt5XKkv)3KpX9FFAx02yQe+Le#Z{s)leVmu`wiEj*3SpZ3a?lh~2BTMuJo$1-0(qT)fX#lj^aI!=B?59>;u@NwcZ!}G-inz2$f5yh&#V% z!=CF&xAS!Aq%V;#LL98lQZM({K0Oszz;E{GSDv%L=kHek-bbzE13&qD zg^tCWDWJg3LKBfUk?-aT;;$`a`!)4~ZkgDFT>9N4ikrVoFZLd(<KAEZ5If zgsFSkRZptUwr?HYJd`+iPctHk^neYis^(M41lYT+*T)`{$2Tw?qD$I9E_K6NPZ<{b zW#X~cuG87WRVItW?SjU$wJOXRS^k8L0?e@X@_24$wXLYeKCFOWzlMQ~Gt7;T?m$?Pt6|Lt2uopo}W538|(i;XW-I~~7f3i1;Bd;T0hUU&~6zbK49vNj9sv57vGv3|XQ z^vu=O&=NEDe*5S=ttkqA+DZ2gG{(Qie8)~!j?#x!hIH6X)!ODi*J6*Zdgz}BhXO*W zFgqx|O?JZhjUiq^gZ712i)4pKTgB-K+BWmGZgm}H%TwQw^8KDjCwSMz*z$>W4Dd5I zOTAhgY`#RC>9z<^qE&zH1%H1ULVS!a&Wh>@NnHtOTL{e^6J(HekjUyM<=3uO*R0*5 z*YMX9d`&_BhI67!3-y{_E#kO>gA;v*XVePvp}`ZkqPEjurgV%2FkYvH9dl z!_8}ET#J4lNS(Si5jr9^yQmb?Fzmc2#Fh9w*YuIeoc(Rs^|X8zluh;c{$lLNc>HB7 z{E?&+;}EL#zlORSMC^Kzcox+7Az<*qE^>;He2(WrQ+!qjUBi*JOi0-Y1b65Oo*Hew zljY~fn+2)E$BQeCX}`2Xb{Vv-W59@5=ZdR#=m5_h9f6(SdMk_T8q?SR+nWtKGuO%8 zInzB3N%*zRTXO6^c1!lVmb&Ou#%4MigUxC&ox%M7b2!qB{sobdvPuC`0s=BK1{nV@ z)b;oeb^p`P!%Ta)Z^fB+E#WBf3Wr5bAg-+3JM>NzVyrc=S z6WKXBomuGkBiCkTGLyJJ!W&p9#o0El_Y8E^1=pV+I$hYxY@9Zkwm2;?Oci1INw|ka z`r?Dv447~fJEU(sNPF}P4F6^FA{j`NN4u$k+9q$9)g-Ir?j06JX~=5-S3?U@(~# zb|GVqm&EDHKE*2vh$6Ry>-x7r$|}PrUEPJnvtLU?P87dd1)4Ol&P>a4owNEXTt|bu z>e;y{uey|dID~k`XN*9378)iMtF+KF1Zib(B`4TYsQ$^1uVHVWVO$3NxV!X1Nr6+% z>c=YA_?LHy#l`Q5U5p)uTB=Td)yBP5z!wlHc3{SM`Lj#J#-t==qbTLWWC#G8*EZRb z#iEu->z;KUwS!dR0u?SBoRov;nv!5Fzv~@YV@J55Rb@jk4!toXz+wY;$j*PJhI~gu z#{~CCRMQ*nRU0`bjECEq-^k195jn=PM33EFo&;7CX1<4RppoMDdqo^g>ws4bxlW+( zgS+)=b_r=GH{i1-24BCcXmdZ$%i&+B4O2&+P$+{IJFf*5nAShRoh2-z(Soy1AQmTl zH&6p2wYDS6^`qy+^K+ygG8K>A9u{t)a<&lyfQ1=F(HGw_q?1mr;v6XQ7_0CHZb~=-IfVOZ0LNKvfJkZ`__*P-3aO>MzP!_GMe#$L zk0fF_RA2p#d#NS!a}-K{e>xBl0WIgrmf>pNWn;AA+ONdPaXxhNIE1&F)~enU#O;)_C?Lf^_e?O&eq6I;xuV!cL+sy8m*^d)^j0 z-N&YbCphB?inuKB-nnf7MUi%#zA4=zRE`@d8$|(_y;jv4=88PyfO=}4^?NA>M2)8j zW9B=a;hVo0Bqfw2IVY+^b?P%G;VJbfVODxAT@Dp_?9`W!NmK5IvO&&l3ElDU`upe0 zZ8y_MCU#EgspzjP@ItG8o`@ESy21}9a(Eb-QJS}| zHgCv>h2JIbjR@SpcusNqQRKo5>XlUo>;tt&Uo!MVB#&;x4(5x#Oc|`5W2_gFg~dKR zX-rFLWm@r^@M^1=or+AjEvWxi$u{rNAKp?_P2zq{meK+NZWA#SI5`)ot(yZ&tzy(y z;sH?Aa|s&g;3@@J@(Uq^2u*9oi|4u}FrST=AZ&Ut%_RWOB&c>GpKPuwcmkdHj=x-W zTDPuS4p~YVWep-Y*%YVHcQc7Mf8KS`6>fO2372*j&gf8`y$NP4DT5kp%> z(0W>4T`@YgpLi;8y@+be?XycShpWi6CjAcs!D=aa2Ic>* zQ;gT>i^zv%#Cj%cIT;2x5mN!Tl=OF@VN*Ho(D5I<+Nq- zKt$IZdN>4S%ro_EE>6iiUM~FWgv*qy08k^X(UmEGfQEBg1DY$QV|(W93N379e&5D9 zpji!szoxOQ7$2wQaq@q`q8A*C5vD=x&;dP|u*UnkSr2CQya>11($=f4)q%O^rS z5=FT@+?*4OE;39f>YM1gTw6AspaPMjFlE(amcfG~(ugNu12q-#W3B9Qw5{xCgLs+_ zfrRyrhO{3papdP&>&dIv$DV;P-Hs$-XI#CAQ7sBJz;a-zlw?5s)&}yPQU-xe0EL{w z)3@dIAb(7{vEr#H#QORIa@;wI0o;*iqmB$VJEj^&;=F)W4Pt`fNr?d34*85c59i)L zkXgw^jd&Blnas@4NT+tm1?RE+TWi8>hSGN+v2YRrTXhnm0o9A5vb7{L@Jv}nnIQij zY5epaAlh7k!#vRW4L!K|XhiG7J>x%uz8%Y+{{W9ypyINDjUm5Y*3A975X48fYTZD? z@wE!gBATVEEg>;D@A*N5(v+G4Zg~~64Me#SZ0TpdR@OYG-!p0oIQp_w^_10;o}MUt zXWd_APHhXZC4LKpN9vc=qLJH+C6H-ufB!L68wS`rD%`zw{NawHt<-%&T1s_>fUzuU zh$sQ!T|@v9kAfa$S*jY{^K?FwP-{E5h@j#+3hH$C&F(aGI_C7tsQ?#d)zIDP(3##I z6P;^BxMdZED6PgqYo12aQviwzv$&|Xs9)X>o1Ify54CGNZ*PARp9;U-Y7*!bj6Efr zO!q|c_3d?@m=;Y=^i;xqNC8ENt(iGiW>yWJ98CKp)F8Lz(K&j%Q>i4KM4rx^-bh5o zefNc&Pp(vxxen?Ois*nq(#T=6C77D`6tv}(ga4>AQ(ju*mF9{Svk|%b%~!2|<0{d{ zA-uy{jJDj=RiMhke`la9!nn&2_7Fl{^_Y3_w*iN$(B9n%VQO5($H;00Hy4@NAI+JX zWi&4vH8H1Fnl&1)rqJhDdGif0ZqXfw@bmSrCULRGD-;R-|)d)2}moJLRZ* zt=#K@&Tpe}60U-0eMyNV<+of~a}l4P4^njtRxwG`vv;Q<<1XT1APHY*(;z#wWz61Q zR(@T}d9TCyvLU|fw>dB!IEwJ9Gmrs(5WUBw1=%qt4>#v2eTV4}1ZC@;r_#3&1xG=g zSPtOAP+DtrMr`(jtb%~V^Z4yKs8TQik!AL?r;2|cS z5TfN_UDQsqCS{l^Y>KI&R8YlUf=;k&IVcm6qS%GaW-WiW|Mf2Kp1 z-2z77*hmR6Wr<%qm`r7wDwjaRi|8gv*GR)X1qo-UU2ZMK6jcp6BKh*!(vAq#ZAB|o zcrO7xW88Lm`Znm#`P1gTB6h&G*vS{syqVWjY$!8v(ARvbx>~cAspv~{W|zTx+{L&O zw&_qf?_E*{!s)LF)5hKw3?Uzv9o1yKi`H|ZfO=#7+N)ke0bLm_lU1);Rf)nDuFCnE z(ekU1^n5pFGdahQZzD#MpWDNJiA|@rW5|nnVhXsppnkgPo476Yk#&!lWwyXrbNslb z<&e2-#o`R43@emNZ67ePcfiU-)=j%f{F7Jp*~rHnm0NXnS9BZn5ue%d{`!l5;BFPE z`XgBQcpvD>DC=co&l7~M-ey&x@aHenqZ6GAW~~2l^?As})Otf+j;0ofoBqs?A21EY z#;Fy6x-&iFG7t(QtQhNEl5&K_=Xg{yBFK0NdI@y)C%pmNYF%!5;H(Fye`Fk$EVAzB zaBEvb*NBG#b8XUE>Yv4~lFO^PU!z(-AmvC|Y-6+WJ0N-HV>TQgy@gA~%^}J$2)KnU zS+lt*+Eanj3R(qe*8L2M3Dr`zEUNEW^sIx#mXG%wwLrD#kC z+Ci?O22Y- zuNYVBZ1B0=o`#|Aufn*@F&n-+x`M>+!Mo_2d3P>FL8zAKH1dENwnB7J%mlSpgIrSE zO8Et3$LbUXJitZU&G8pLp6CQ-L4{Mh>wZ2e^a)JsT~-f#0{-IhH($A5cd9ACr_jJC zY+tSoXz_)n6-iK7Bekxr%K){ZI4eX{GT33V92TPcni1Z^Nz(Io%873|Hy>~Y@(j|) zMtB_!@y7b&pMp3}n?j(w2NjAaXRcuWR>r>=90s@B+%oDO&6a1ZS$fPhDMHS|8w0(H zqVE>y{DY+NBw2+i>1|E|F$hMFMR*9}K>Xf^CLWQCFb;;ZB^wN$0 zEF6tRAcQD;8&`BEiReYc@FaO}>(hw!+TK+n;Lx;N2DHtzVloEb-S_puwV7H^y|2dI)%f;zj$ z0flpF!}1JMK6^LanpNo|MjS>Lg-|z^d@@om4MdiIcQYj-Azz%z(8gvUO9(I1l9bez z-}>w>(Ljn^_2ce5kK<)<_c~MWFVL>fZTQtW&l?GA6q04P%rkTf5{r$8#0BXOulW34 z-IW!;=&td36f4>J4e1`oKZQM@9_M(JAlzn~kmscF9UxvH6Pf@WbN7`KjHgdS2C->T zAQ2ap!Df78*dvUvzw!Vba4cx=k}AnA(?&Z!t?~~%4Exex^MUf+qV{CY7aJqfq-=({ zc$S+2ckG<(<btKP-7^jmT89<5y9l|3ZH-3HT7hi%N^Q zGJ<08m*nlcxuJR>G+}m<4-oUQC&TXexM3Wx9!M|B7=OLH$>yU)Bq&~64s9`%WzFoEiYz(c3E9#y2nkE571~0IXdCR zhW1C6a}VrupK!f;gI=6pQnlYN==bRm*CfC4gpG@LW#!ce)r+0D@1KdmJ!~OCO%I&H z4Hb+P%`y%Jmnq&b6!e9tgoVE((4S`G*;B=*dW|kt8Y?fcJID?|sPf|2ov4+{)AgX; z*wytF^F&C~NETQ#t!Uk0im0^=Na&Ey=%eAO@{rFg#2MOcg0=#prvfN$Hff*};MmO2 z^Kd3;0sY8nhTw7#IDqAUEtO3=#1q6bR#easG7Cd9x^A6^aWhNQ(6K;<)uHU*fmh^W zJHp1ZG#Q6dYGp&Xoiuby)P1UxS>Ym2Jfs`7gU&Ys_Fg=09Zugcae*9vjYLrl&owF8 zE%S6^3vT_;jB0r=rJ&euE6R6KNzvb{WLuP4lg3h&lPk>0RixQ7?XK;e=H8;1h51}> zFeo8hcr*Mw#l)@Mcn4HRr25qt{w2D5pU1-MaoY zYVE?c$?&m-I)Az3*TL>Xw^2(qe1D_f7MMm;4~22xWn#E3DriI~nvGr$;n}9r_q^2H z(MZqVFX~A~mo!Gzw|87lDddorAlAj_hSeIJ*6&tS*|NvJmVyb|E_Wx`Bg1T(J`xi2 z0qfK%-KSe_Hm$kp0lavCLXTdsfSNu$>??(^etgGg7Rf@wm||JGM31P}cGrwvu5_4` z)@GiuMsLZ>Ox*@eqFXM`Z4#C{9Abr0!!8DU>}TDgG(q!(qpEK$CveNuKbC|s%onPW z9@>5kk6|x&TnQQWEQ=a_5eZvkMzv#G?E_)Kj@6keR^sk5ti;^!HBdM=Zta2k zUO1?~M%!;%YpfrP3=kNmP`U(?T7%Y`3K#7&4EFI)l2C^F2&kW>{n=~I7wtA}JEmkk_q7wag19~GB7E7WqD%0=7&M`ubvICSxYoUgglzS)0}{4c z;`TnL=o2M>0x62xz`8xX^ECEqXa%jm<0&iW3q+zN$L091^HH6cv9{UUO-8rz!%#iw z<(<$`+y~+GXz9D4(Sze<2Zd<1X-pmGEA@+eRcya3O~_M?uL|>+NwbFWxG0Ea6C&ld zZe5XI{Bn5_da@ziq%i?lR1~BpgXL3qNBXc5#DuB7M}(P}3JaB8EEI&59TwStZ{h`J zK1gUH-`b{i2s>)BGkDZuty2rSq%pULW++an46Slp_A)3QX%v>d-nd`$a?c8oMo~?_ zuxpJ%QV0?Fuy)>ZVR)W1B$dpd#Ez(s75NHYK`lj|;q7sfVgjL`-iR>M5%r*J=Z|)j zW=NJXKAjW!!(qa*9kg>cp7Q4FF#{H2WC%^`7a-CMxde^1V8M$bXTXY|F<@C(LA(yfh<3qcBB zWC(=o+h4%)#b&{i%d@=LSmeh(w8oNuYv&xKkLf-ktdm*T|LtnLH|uzL-z6 z+Qbe1+qs*f^zzC2sPYGZPWR##x?4R%`vuxKyd=t&14m!@^M(@O(|nd|xRT{ON^8XK zsLvthgndCgetB-0bgq}I*yG^^&a3J#1nKqL4A>dQl~e|JqlVj&8mBr6G`;GeplWP0 zzG?gb+x4XUF;+@IG8U=I@^m8l%(cWrL214d%b=HZr+IAJx`Z;p`|HSfY!C$^|4ITq zH*|138Z3AgNng~V{~ne;2-5lrT@~IDY9wuR{pm_?(_gWiQv>Qa6?)G6z0SEafXjzpfqM4RiZCGCK% z?h)eW5C1_nN42n``T>6o5@0tC${98lv=AN?bbv5Qz@R9v; z1SRjV3=VQ!$K5Xrju=6yBbCj*#9+?vF4?8xM7ljT!hjosxB#m5w=3w9^(az(yX5<0 zx<7_^#xUTD)zLn+5PJY%4VV>DRDHP+z?OV;)Zl}W7=HIR1A3kB$2cG zrE?BnMID4{yP1=S9GIEV*aScaB)-2iAjvW3aWZGi9R~Nd|1wT-Oug!x>B*^l`NZIs z>%X#kD;lv1Rs$VjYq7Gj1+q|^>G2%10=viuT}=%XGcT6bwdeo@#6}#vSK)Ynz`T2o zQ*oSp!poM?>a}}C>c5nNKvA|#KxF(y8RZb1zOQ-4E-%R!f^bpfvHB9eL%>5v${qiZ zq_FIw_w*ux`Jd#%50_PhlzgJO*Jxt@dPw^cEuT+MNzFIF@&Y~U8U}Q{-exBj&>ebT z7na6~l6J|b3%Qdb5SCJ4ADrBjd3ekDcSjloI%}J^Zq`yIa{qmJ!Qae8y&ol`_u)DR zoWB!)a+p@w%r>$vm4c2cEVbdOylnkUV{u=zb8G*Tt3W>v=XGM44*yVmeLdGF&tJYF zlxjm|#nI1~fY0NcT9zN=kqrh>mmdnj>$Ek*Lf}m3)iikdF$sqBb+(#AsQM^F*oTg^ zVoxLbH&PEBIkAx28E+CV@)TYhQYU#buq5&afa=ZTv-kdPM`SJH>f2FGgO(<@o|HgH zQ9XG^zIdD}N|pxIhL#}-E8A8cZ=(2N>vo@N4M234Glp^X4%^8)E6d{}FNo(SNnvAj zSUg~+wTvGR47TxbiXyU_#F}bud_NTTxW0|c-^x6a3?auIuWork#=5%H;l$ZJCv{D> zpnm7#Jc*~(An%1kT-cU)zxA&OzI*{xsSgNvj(hg(- zd$(TS_!tRxphYx4`-e+Bkxu^1NymPI2(J6hy`2|Le>T2o9s>QgWM&%j$(xP_CQ8Dm z>Mnt3cbtd$F`-XJeHq6~ z>w@0(3s0s$VGOsQ1;CjaG-*e*0#SUD^Dh_ffB5|7g08X4eJ9|94+Q01hLd^aOFdh! z!L1k+97}fla{2oaWC_`S2OuH=Jce?MS*U!=7YA2%euL-&j|nP;`#WwYL@Wd%ph^+e z`idYF7o&6=Ja&_1Sy;L5$|56}Wt4C%!AptpDDPy*LrSWI>uG?{*K6=YG>K-=*jCK9 ziU~vHR!)f6d%gLzM9~JXKXi0E_DT){E&YwjfqogA?P^Rw_Zu^Dnlytt9_sXGrPM<^ z7{3@$3+?EqPRnck5omvvmT%f{KmYewU-Uu$OD8Y6PxrLH0hHsd%oTGRnkXfeWkgp! zL#khG?}yRUp`Ct$X8rH$Bi3KuU~kLg_H&9Q$&@_&y=ak2@c4#xO{6f{2>4$Ys54GNnD$OLH#PI>KZ!`0o7*)tus(Vb#kkjP?W6g_ zext61NOZd&=2QYozG`XN$VLtKv!D9+C_*Hp-;$tX)tQj3_fhUzM&#pUnqYW@a|(*D zg+^ihBerzJp!gNZ`J7jL378H{oUfxU9bO9%zf*WKP{j0`9;M8ppIFm#vtW;QhJ*l>d;(xmZn+$%RVzV&Ug(;B8UkLK^H6#rwJB{(!bN2)E2Ulgh03Yf05 zbnzw@Jrar`JviY)&LB!~qj?mh1Y_>FTt`G8!*q;$xy46)hp{mstF_x70&)UdILXCq zCj3}y6MnW_xS~w2gi#3|H8+s%bPpK>+KcIC;Qn$XQ!^@9H3jN3*r+eVK0JCQL$u-3H=D z-;r&j6-Y7OlnUqM+q_t+hV%@F8GMYm`Xv#ozV`;}&D=!MpD2c4HCx3G!_8<(8>rr< z6Ga|prj0?WkFn8nGam727_od%LsO52!GqfNKSr_+v58fnj`Vbg zBC&jX5g_?SA{r%7im%@Bm(^H3$MPteAS6oi!m5@~`|hAsKNs}UGzgrVb5oK&HTO|I z6RS41;}eOXq?W=}zH1Zj1FSHsF=cwyqEx^5G4QHLeqi_LV z+m@=?kyYq*02~0{vx&Smk@*f+n9bDmcHo($)mAPiqN`0Yf;jt@;a}Ggc>07-BOJ_A z3cPRPdjNKN&d5Bj_}-IVb=CpkWyWAz3p-WYZ!%kyQq5?lCmPtgwbv|+=FAOie#OX$ z@e6BR@b}~Q%fA&n8@n6wVt9Fsk6i$9BVe(&tzW%(uO`8^# zBpadH`z2L4l>s?tofrREU3(1L=U_q&Ns3%^-Z?mv<#P&FLf6-q$Di_nvv82aNwz-0 z*d@k}Z_*hirj^X#R-JJ}_wxl2J;)NpZ4+WM7J=~H2$czRziy}K&tUUEXP^=CyW~M@ zjuJ*lUT;QHXDfLTao@j!!Wmy~Jv*z+zb%rShQ;cbupSTUE7ssfGS1C8Ns#kNa7HvZ z@4UnfNh?puFAa)?@ix7$%`<|^p}H$}->qBg;8=jDYkXeU(UmNFiY3W5vl|^5 zo~*R?Nu-X;m8Pr>2=fI@+;)V?zLUR*0|Yfds3|en6x<*nd4X1~r{!_lS1(DtDWp#9 zwm!`f-C2Us5W2m<;a$<#FcHBPAU(FS?B$7__6#5#kFb2ys^WI@YI_XLsf(ycdloX> z=C#2$wk=Kpz;QD+jxJ0gYL-zl$@&vTE~t0rWN8>ils~tDVM_!X3l~w+Y*L+fcf2o5 zuRbp`na|}K-dDUA*squc%~6P|-*_XWpKXsy&ATmpq-ZJ-j-A&NhM`!l0;sEIle5Bh zZyJ5N`}&&HOHyndSMVqpll-t?*LZrmm?h*6^OtsI3JDvnELro2c2{V#i-bP=po;HU) z84$<>dSTg06s~c`0jPsrYqH0}GRayJn2$ka7Ru+ZyU}0QzE;KhZ07hegq2+6J;1!} zbttp-iwT7FjW$Wb{a$T}xbzcxdHAG=QSWtNzg6d@&-ut@d{MWd{(~U?9OwCQS zi^<|K$oPFaDPis#m0-vy$NMTG*!4qd8S&2*2F8-YH+fg`2-V%CF?DNI>z0FEs17=O zf8|e6^2n6Z0;j$u$>^^<|NGdy{8ak~+ID#hXHT#6&5ju}rY;+-$&lgGVd@5Y^Lm0u zjkKh=+SojtwP6El!89YX5epRX9bPGf6t6Ku#HDSSGCL(~u4lmRzv+cQv*}6JC`Y2$ z+twj7;iR}oD0ZR$huIus8CR}f9iL>Qe<%{ zCe(7|$Yp7*y5;D?hZvz!l*-IC`w|^05J0ee?nLD2m`J=tF_HkHB~bEWlH58GDC^V& z#t*et6KxYuT5xMt=l_P$Wa9-&re&gWj3oiAj&Mqrgm5}e<*4lo4L1<2EXf@g9am&W zz*62<^X|ILc_1ei1CH+*2i;0H*80|#<4de6S}c$QL^<6>a9Ne%m7q&IFw}oG}$1)vB%(52IZ#gXWrx4Mg!zzw(SCo{fFXZH z_OJPTHa)gQ^F*g2>|}#?;l7wGNf&Q5pFveA_9cGKG;bL2 zvSJXZBW4KBGEBQLvXRmd(5?BeC=nbR%x@k9W`)(VX_p$CRpU;Ecs@66G2VE`!yrs( zZARG`mPoPn+aDOg`>bprX*60Go>@mvQY$?JxmGZEHlSN=^1Ch^6C~6n+`p8c5qy!j z_FmPm;YP{@-Gk6gwlAatTB}M=)8U(+s6B4|RVU#UDOA>k%E|MVX=0rH>VapgF*RQk zul2&rfCJ<+Lm|4w4~3Zr7s$FqPJQy<*_Y;vyr=+45CM}jgqI7M6ajJekthIZ^vVc- zxSu`sI_tmdz{Na^%s)$1&zvqUdCl{hr`mUOrbLw>*QHLXPzgca7Dc+TGiC>{Q->eF zC$9xtlrDs$j!Cl?|Bk6=`n}X0j-&)en)LV6QR^exg~>5li8(xS4jgl@QKl<<2DD?i zux8!WoyJ@H;5JX42#DJEeXtGKnZw1N8e(8n5ku%; zsFE#RKpdmr;wMJVMbPi57K-QPrT$}n*3i%8+n$+%iGTCQ_mR~MP{F@Pb zmdCo{F8?xV-GA!9&UBT7(0pJ5J^&G*C7<27GG18UjWa|$Y-dxRg{`{^d38Xabm)KR z-_XGJ*neDs&53d>MpT7iDeuX)M-B^x2Mt3jKWJ&=c||!2mWqo&So4wI^<1mG0K^4j z)jw>?XFCKnVe=~eZaCTr$H;Y;`*H-`hBEwoV?}@@;E{!clPDAl00xxc9?s;W;~GeI z3E{_AKLX(~g{%beeyNrwr0&!mfgccjaUFbT%cq*$pdvs(tFNOc_fra0raxi)nDBvo ze9ojm0`sM~r(fjW#hCVHNE!l7#n$Fzt_0XyK$W^=v?3Qv>eHfo?+kq|^U{qnMXa$k zRk5g$bZuzTjtVcsEPT<+HUKKY;mQdzyUWC>#^>yEfVE4;k(dMI{#@b1aHBp5SjA<; z5eV@#nVl2DL|TdfJ@88)&xQdqG-2)MVn9aHsPbJIIYkpKlLEfaWS#sRPdQ|?4<&4h z?AtVM&YoOK)-dKA4jn_*M(bd=y82%=N2=2f^|OIrN*ukkXzd#C+5)c^p_8R&334zc zs{W8HFf&9K%sngv&ur4(uXnGxr^U-6e#ejP2>Kms_yWzhQgIH72L`sM2V#90Bm>Qj z94gTO@5nd2Ji>rTmWkr;a;GECfb&fRXK|Mta=^x!m(HHP;s~vq8 zX%886G4P?P7Fw-0$WI1rHw2N>aiE-$9d*N?#pMt<(51vwV8pZ0?6v&864${Eo7u=r zhA%7moRje1;x$Floj2fZB6u>pFucIJEm@OEAm%FaYR((ptn{`oVhA|Bu(2s7&E%%L zdAC%eQ9_!%A8jv3q@TNXpG`(X8PJ=Fs@GTz^sXg`qya?OZ0XdHO9icqP+0&nZcwZo znb#XNw*o}*jpuR(`^(p%H7XbsP1&S(gT5T%_<<#(aps-VpHP+^&xpgpGt>Fn6sFwF z1ddJF11kR}M(2mWY5%)a*YvBWutXCUSQJLcbDA#j#fMGeDd}o3B|mo$Z5E?LJdJhV zet+nz0g8WMg9@Tv(r`nvbG}d4iD}%8PGeWnE=_A`43{M|>lVlDh}pZ)1O2AZVdiX` zWyi7q5@v5e0(!O253Za=`H>v6r@m=qx>$GQZ&P(|y!`P5nr4Zm|FrG&;-bZYGDH>+ zzykMj@zZP}@hUWSZB!2Vu8ZZycsloVK&S?3xiUxYSz*pKD)s>HRH{yhFOvv|=>g)- z-jn!=Z?^n){^i}hH9&F%SZK?0!1e>*c!Yf zC7~mE=nd`Vy>j#LV0M!mm^q^JON=(P7{(9_l1{B8I&$J&M@aR%Kvgv`acmRr7D{$I zzYz+za}6c&qrumFv02OPXC0N)v6s$_sj__C<~$icOG13nUMU3xC2q>c6v?j@NhiUw z-BT||u!7z3rvU+YV!N|GT(c0w?wCo};g$veyum8NmUt>uMpcr`UC+{>eXzaOPkcq= zEOkS8OYu!i>^ke?jRaG~gvwie1TA7)cgMi01gC7O*mA*d>zRPn9zczdoCG2YQ7Mae zuX;0MGuYnZ{>h^b2U!s%Us}*{xF>a}{xc$l?tZst0TFB47hvav1>t@0i2V%k!i;c? z!Gah1jsZJCeU^c0BZK`Ze6=bTYxh2pz-N9hIP&oF4*Z$bQxdkT+7vyOn{NP=y8DaG zLN(5Ad~+kaEd4Q*l^PY2GTj`ZE)~FKPn5TlnT`{fyAtsWWOY1N0*4MsCBM662S?23 z&O-z-jyS}|Cb%I9_jmPv6k0v!$vb(=!Elww`Msr6E7?maJFFtB&yc_G1Ov>U`Y(J) z5VfPts_Kw?JAnqOtT0a-Qdzs7<4_(=f=p#&21Xq-Ys2c$JBDY;rrKG`6^axSS)}=& z$qFQ`skDH9)^e?ggWOyFAnJkm;{0}+__cg!Bbt&H{gS5lN2c^43}+zoH2+PR^DYMjf5}uB|7Tk8aR8C|ql^^m-bx48t5>E?kKz|S`L90frG-uj z{k#d}Q7o$5xlN;=gw&BKM(`jcOvkDcz^~{1svwtz*YJ3RdF@?LRzog&;)o?k=34*V z^Z_F6Yg(&BLZn#*RTzhZ+>dHgj3@JQe?efPaDwZT@+F!bSi9uYrXNVjip2fllm+oC zWaKHdYuWmSSx{Enisb;twIVYQ&$Xb!jcoB}t0g79ufGigLge9eHjkQC4FO#<7X?9- z3e+%~(vY$?gru#6LwzzxUZrn-lu($7`rA~N7DR{|dK`0DWH(+7(NiLsr%OyXw;qer2Zl#QYfA zBG7^5CVBVYN^~IrKG&}cWGXv%)um(`%W932Znt|c(Rvnvl=Od!f<`--5yR_zD#fu| zGWj;cOikav+%2N1-gwVtbRlmlSM==Rk4u!W>UvStXH`V3AKetF{4BeBuh(_JZ1q%d zi6h-8DdCIK47SoeK=a%cJM)mZ`RmsO>gpXE28o6vhnQ_;03$%$zcEI`P0TyR>i6?Q z63uVmT@4Y8m>7=bw8qdkZU1ZiBH}k3iA^ul3LK1HjGL0O3_c zEeFZ#{T~1)K-jYfHjWL5Bti_RAZrRJYIRqE&NE zti6!LbtHUgS5A<{@GEhSEHuPy(`tFZRbeEv5r?{u{w(awDND<(<8p z3iadW!qnEt_Jb0INzJPtjYXfwHu(F02)!3k6}*0BfpbUK>lD9d7e2ORspd1tgxM7c zp6=GJc2mu8$VG`#i5-bSB39kwf4f2c&G?~(XBAS{OFZoqD_qfo05iZ2iV#q1)M2q} z$>n*9OId2{Dus!5Ia73tutV#{`k;(a$}V3~CHw^_&U4In5G0SBLQB?ipa~c+n~t*-<`qWH?{%&4S{hUbXl*EJ*1|@)%M>yTx*l zXQf|eeii}t81)^zxVQMFm5gmj4@~{yOdL0FHYxXXatB(nwS*!ur9p5~ zv!phE;_GuDJ^nA~#t*hF#Jt2PtQV9Xo7Ltw_@ITTmz|1#|3|dSOH??`TbqcF5IRG8o{3Ia`3=76f0tF zD*9=>s3l;KLR6-)8ng!>iAAdxsJf6*YIz{yGoZ_r6IN<|Z(EfXeV!f56}zEC07Z}} z*4io}T|IB@f`ZVWHM$K+D(j(H5F2!OM$?t#14^YuRP_X8cjl7c&+hXAh1uk#1Bm!T z+PYwwie4=x^QF>4+m>WZxAgJgTkOZAV@|QvHPm$fr<);Lgm3DsQ^hMb_~mfNI)C6B z=Sp7o_X}eZjWgbFh?n%J@5+`Ynm;=s1UxjU2Im}F`4`ncy`jHT4QA<@G@1LRl(mp z=l44o4o25|tH(dy9Wg-|pkOJ@<$$ns#-~mk z<)JkZvgrso8s7XTY!Kr-T8SSUhY?G^w#(07gNbEb9AAYDOKGJH6CA)WOF1D}Bkn(0 zTa}Y1%}m8dslst*fTTz~q(8i$%0Gax0=W3lNdkppJPScm30)?Hs9gLYR?ys{jB3UN z8Jsm%_r-PfypTc8GMbQ}3zMTjcp8)nuXWpgg9wH%&8X43rFBv7XeZrzxqi;zIs$P2 zNq9K6QII+>%98Sr<6#TW7VHL4w|4MY$jP7d0LwZ`_yVx@N~>slOQw`{E_enevu)d}ck>KRG zBKU%@UdsXB`Ik~4t5%SrJFb}6stg<$z%yCfMQ=@`a9QZ#II80bJWFRYCW)fLz{HqV zD+HXqQNvz9_<|lS{)=54`t7X{qk+K_JOVz@=Hp$kOY_^cw zNBc#c*d&Bo4vBoI>qe~>qZna-oDGfWuEb+9K-pwrhCX(fjJtMI3v1LrOoTXYeYb(& zVCh$JaRM%Nj8z+c-%K9Uxv$0<)M!+?_+vr{b^Z}9It35H%7c6EbHcyEk9Cl!sw)mE zo#xm0Y)UauodiA%gtH;7Pj`zK&`PA5xcYYTlCl`iSrcAa?xlmvb6sP|=?{F;eAe&9 z3gtY;b&%yu)6pRIt4Rt>dsNHQ`h|9>)Jsf$&0hAd#fU!ohk5HkWu-4O!rU+! z4)#7KDk>`f<(Y8#Ui%xDcK5wygaN>tV; z6iQVBfD6!#K+Q^DdB$rC(yo7JJLEA0EsNxUGe8dQ2<^g)Qpu;t>l>=^8oM*=DLt#U9JD7Bi;)}wQSa#R%CJii<@;pRY)@Vbdh&$ejXl<()-TNRkOlaWaK8@i zzhmQV&|VMK0qFtuk`J|__WXBzweZBSU+UTScqgff&WyL4(oC%|ME|oClYj15K!50p z?2OG9h=siIKWE;c;FNl{5XB{m@04dk5Fp(%qD6z6)cM9QaUs!PtKQJtKwB*ptOYq+3b1r6FFsEMG1h4T?K)4E3|cE$fGjK4TtWh4zCoA7x~bA z*cO%G+Mg2OSZO*B^iShSj1)kB?t*k^WrmF{JRViBE39*xF$lVm(NyRpp}wWwo%0_YW#DrWwz{ z_-H?pl-8d)s2&6?EMR+^iQPpoFs# zc;(ejuxn=s1PwBFc;aahr)q8%mNVTd+-PKtRFXQ99-AM=w|p&@XGV*f!-ky}Sm=V155sKwkd<$WUt5;CPZr+1n*9Qd} z0Xz~@sa6=v3fqUwPRj$0t@70u`K;%_?lq#UA79ErD0#?~#UG=uV;M_V?#tf)0kueI zTiJPhntKQxPq0N&QmftnByfTLys$IX6HK9Jq|V{ubjsZ!GtIDQQzS<sZiE#-Gs+SFpCsm+ReROlu}`Ah$n#r23akYS~s0)u!Rk7xZ2=Cn8N z%Fg-T7b1g}Q;8R{xT4VAOCPy*tDt<*PN=3%|Kv2o+Arh{Ablr1=l<<;xXYv)jVG&T^10Z<_aEL>`GDQDO654F6_ zKSImVIXK6yNxSgoQLgeAYyiyQrGBo!lL=$Od_y)JPrqrZ3FP9V3W@ZpG`3w=YGUp8 z0p)IPY`Qw^3-JE)5UjuG24@rvbQ(U?RQAUITT?k#&lX%grgoEe(T<~P_L=w?uI>_vvR@7mpdPMJP6}X zUD=^xj(ybJwEIpDz)8T=m zsE{oW#QlOFIkBb?40~r`jBMje<;a7(wICs|h=-y~Ga-Nyr(bl$s*X22TTiUP`(*#L z?wY>*hUkDu>|?rjiPb7fIBE^zeTT)#I}`cQwGeyhgng;0S3zl?Q`ZY{13dk06j#Nd zN~|36O&y$Gq7}W5kkmk4_Azft2FcTM8KTr!#3Rz*2PTqsUMhjDQx3-7(RLYRjc=#D zz-Ofkk+OxN?kbh*WVGE&Ix6JCpk|-(S8U zHyKe)qX=ns^Zk+w5nFJPn?tnE(ORNo#aS)&L5YtcCt+HvRn{?MB41}s=zMQ`ge(Jd zLPVK;fY|c+DwK!iZ*OReBUfPBce&9wHF-#*g2)5fGx(CGy?kRd&d79E?$en68zTvu zvAJq;YtrpK$h}x%o4MV3h~*WdXc4oC<7D)Li4c#Ed-?+aFMn|57dZhg45)+@X0gIi zH7!T3l7ZtrM!HSA=v^%$rd<+ErYWuLR|Ao96+(o%IXINcAC!=4k1LM}|Ma5!JekBf z8}L|}v~s_iMc=)A%KiBC_PPKEs${m7GxCxWDRA$n!yv&?l_;23PFbcW7$`|)h%*(_ zD^25CjjNR{K9wqY%ax1IFk{%1Ls(Hi0oqggmw-ZJF}pUeA47DYa(yY1(MLmmLTLGD z6iJlfP~rRy&ssh5-@+m0h6+T9YRgDuSBF&g`Na%F{(67GH0ISRHm?z5Wsu^@R}-R7FdGk$aQWEA!?cI3olq`k4+s3Z9Q-x51ZPhP zc_9-qI9p-wm=+j#b(!sY826dycR}rZ<^p7xbr318zJC0xStcnGyz3>0;nh;a33Lah z(maU^DJ87Z?6WDXnPE|Fh+hx~RiXhFTn(51}%-n~!y?Nn*6lCPI)b2p%cjeMMm9Rb>j4HMJJ2loA3Yvn8lFZv2ME%o^6^)jiWGq0WgP16 z#x|F?O|1ls3|zlen_CoFsy{_F>W|#SkE=L*KGOHw(*@o)6Ch4C@!}R^Q-8K;NMn@w zini6M4R7#l(CX9> zrCIr6Ri;rr#4YTRl_G96u&&ap%6qRu9MkOsEET#4p`*n~Uen3F$YvoaG-u{t4x3kj z$CP}yp~?wI-)fh(EzmZ3D5dsHC%2^WNC<8+?fr$I+0_M_G7-ylhl0ZmeOrXg_zdUo z`?gQPGZh{<-LAxmQ3by+7}c4)&Tbzybo<9u=MqibJgeB=*Z~?)!`UQtti_w^e~LB* z``y~>rjhn8a6N_LzG=ly#=Et~foIx{6?}-Prw8_~=G6D=o5cX8_|F$z?nrYY1?{P95%JMUjvkN1{UhvT;PHb5QgDtnk-3_r3>O>TWQEetL zApbj^J_o60!C24NTk-$B;pY`sqQ ze6g@yA?j8LE`^T$Cl!m0`}jkWU)uC0piCgq6B5LlbAGt>Jg5t*H{GUC*E&w%1_F#^qQ=#M82)sL{H<6+$F7q&Us z%WJ45^qlJwGAEW3P*a+VXgojTqZDUbls=Grm}ffvA9AiP5+S$!#7zV6aIM?JLs`Po zq+?B#@$@*3ZjcVR#_=^k(2HBK=wXm?69A6YzC&$zy04E7XO_i563RsA+;r><=V0`y zV7~%(z?WglW}QA&TaznJUSElDMP!F%6eTH#b?A)O<6U55izWN>=l8IZ@y0}(Sutbg zU%=d3@$_wzC>XtUq>Y7RtdX^=Ae!N+M<}Nik0N+M+oS}kNbq#faG>BHu{SbpAKTQp z`CD;=2k*V#Ej-nK#oQaydVVwuz5{oT%JPWHos$FM<@u6$f<}>iuD7~D2a&uikI-TdUiq8YpfQ)&sxwUPq=pCKTTg)O(jOnCG zDE`z_mhyO_Z8u%yW+vdJOs5~McUcsMf_CfPyi`0Pw3{J@-7}4Gfd}GFf=o_1+*1B~ z-#do&S$Wi(4R~@S>DhA4_Rcyo6QG2T$(tFa2UV{Uvac~zvVl~ThNvQJNjNQ0SSYr0 zOU!I>mPo>x3eT;09_{eJTQ+Hn@J5F~nn|fWo`zGkEq@tef2IxU)B< zIUU!zzj`y=bQQ$$T^`LIAWSk;u=-T+Ix6;;hMUau^0|GDdD|U(PH5L&*P3BDh0%%i zUHe#4uY`cp@MB7AZnJ~OVkda28_PByiO&pxc!F=?RL#mI@Wm++RQg`%BY4j#g7ufX4N6hvs zjbz!@Bs@JVmsz#z)RINPa40v(Rt{3wSgr+9bQHQ1Ej=S4pJG!=-BhTnxNip;A~Q&E zK(c=3l_`|t8*Dzni&9mk(e2{lyf#O?#~ry4K^!FPdVV~nZb6lG+ff&wuxf;d7Or+@ zf!*3-;qQUi2<5bumS#IV2|}@wdm3eUOGn#-6b_%Z<=Ez;V}T5>J1d!fQHj}8ZSA@? zq&SZt?<}ndDjLD4m@EH7hZ2QmD$HoH{-_%X(*6HCycU&nn4eB6b*fY6>U$}UTnzT| z3)%Hy=~e?wK9gAM($#T4>uFjEC>kR~=%tk@#%2Tq<8J(?SDj(QtzSy0 z?fAS4XFvm<*3=_z`H^7-SZq5;eClB7)pSSijE>MI6cZc#@lDbi>f>QVkfc9;Dw zCBk#HL3_q3x9dr3KLa`;4W=&pmuGz>8~QVh#sIDJ$y7QF-2_qT^|n{=aeo$jnMJ zsfNbN)5R7u?!~Ckdmyavv8UOMR-BR0{ytb7_iKRAxirrvBWt8@@dNmaUQ7?7& z+OaaQ0rhttrak|inB*izsMLmAz1aT@=87zPR2MJZ ze>az$7CEc{KstA*He~{wzq$u<;E)zkeoHNp_NI}@b9Yc*I$qHm4J0B2WF_!-b)5z$ zrB1d#DW%3aIbCoEIMhRj>AI9T#9FG;F>Q8@Q_?Nau5(2^-iQFGwdar@YHjrsc^ysf zl20$mmF|olEwBF5SL{Nmlco+lMkQ3_%mppk+Bs@ymmcy%zi4lf5$M_(U|9VlXLdovx;p>{6db%-_SGgf7!Uve2 zqGS&V8r0D(AO= z@9l!-*>m!>;tz2evX?&DqwmsQviV96mRMGVN}pKbF4DY>_lv5t7c*F&n->pLo@o^_ z^eiVAQ8ej~{NJ-3ede6jS9}+hoDEc_T`HM}fu1@L(D9DrU|)7m3#MJGn{JWoH|%w7 zj$7NwIz*&qd_+sAOhePjJW(RgOaSeF;_c|f4*^XHvjmuwk9hNiI*Umw5#f#ml5jj= zmvb%u%Xv*N{xDJ@;zjQcOBP~ejaZt0;r`(Qgi1|Wc0`xEsHz`3IQZ^D-Y;mBBYaDw zZjFH4$&K4~R_)gl457R(@mS{4;gE)d4#KU*f`ejfOFjWaAHfaO9x5xX80Mj9(}b&> z%)ED2N}#cWPdgR1FUG#+jlH&Cx(Tyfhd{U%?3?!TgM&wSn~;RcoVb%{)#XKBcR&(q zz02*^0VMR=uE+Jd>Xj>{KG=19HJv`~nhD?*vZDn3*Bw$y-%W-_(F$qGRp?*od6?A$ zWGBDc5hGk?&KgP|k`2?-xK7ddZuX9ALhY3d5|}S05;Ymutn5b^|4S4fN4#8$r^8&k zMT_IAs@elsx0n_>$MX#>$RP?Yu1#TjqR0gf{@WcN??X@WVYcK3pSOT=&=b?r!hjCy zJ8v8$?fTdiBEdmG%iafJ3gg%+MjAQuI*25FaiHP!g=2)?Yc|BPjJSsW?OrJwIkcS@ z8fQU)+9Fjg?&pHQMH-EC2a|0rAR@!68Gnlb(I-Mt#^@l*$U&IAULt^9O)WSw7Uwkz zMTR`y3&6GEfqN-U!OHokyf?mMAk#NugXrYiOB*r^4ITKiB!@yWB(uGGe)=_%;dg+VTf!l}r&#d+gQ)`*%7YD+~jZ1sBKv1AVjo0sK_ z$7pF{i#3YSm7GP1WY=U9UUs)owh>>yg>B&xMwd8feb(QXWL&>f*NEcm|5vgvplWhFUz9N=c597RFf%jaVgK4TZ3XOd)Kqizp zH!l_2?y>}HG#cO0NB?B#3B?AiIxH~}wfw_``zN0cE3qoe$3vEag+z`UhfCK>4SZ=$ zO#n05H40&}zCPpP?W5`<4sM=e#Za_8sDlLL&d%+Ge@9geiC z^6B?Te4fy7p0w<2(cZDT{q&G|Mrdr?05tqZV!BIqUDjxd?8in|0Z?S4MzRNLzPSBfdNA zEg)d(;&ACmOi78OoQ}n-;Dz>|&F4fv%Z9gN9ZOa12L?KjV0^s@rT3nozD(UUohvxRD@9sa{&X5fO3m569Q%~i;xQOwlGV*DghZU2#=TB80^D% zbOTQVby-;LdlG-kyo_=AZgpTd93V0+A>#&{%zSAV+}{EjZuC*Cb2nO4c+}T9{>pk& zyWr(UI0VY;&`1_N-RIG7+~Aq}-&eLyLe2j&?q zOlGAgC4XZjQQcEnaUp^(5+_c8!P3KURr+x*IY8=-vm;RIE(wJV1|*zr;%|p_0zyW^ z-=)ad^`q2I%}~7gVPSK4Un?!Fn2gdHblP z3g&5(Foh_qAj&oNrWFbB_=@c?ZKaWBy2Z(zS}}gPP$^9Ka|5!x1#OrM>J`zZ2yGYR>3gzuTB>EiAFIBPXa1Wn z77E0TnruVBG$cldRoM!S>ia+D_iQ~7bOXx)5V6y2QfHv8fWoQ5SaDH#?icZ<@D99j zQYtFzc!-N5megGG%?B0WBOcLcaRD~8L@kx=D+f-SRgGX{f0lPnQj0?f2#53zA3F4e z5J<-y3r`@EM`;n$XqlK2r$G3mI_0PeXVr!8ZY5X@25pV=T7z2W0&x%CRW;!8Q{ zh)f_lBoRsp5QJ$Op8SPTNY-dz4c@@pFg2W$uwc*-^5xQU9i}jO$bmFCGLzKR?E$WRALE)q?%f2KFjux4k-EfaWZhx>@tq7WIKJ>(Gq+ zNgL4bR=DC?%d`^&WY)_5E#|=&-EoloV}Hl=vIjJOyWSm*5<$wh9=m5W#l`PM2K5?m zQ6xf_6ELor%;R`T>BiP!;_^x*vE+C}L7f zPmpFUGWSJgbkRhuS-qRMC%xv%@amHY{QZ4aA*7lZ!;2*`!H2NKm2=80+b$who23T! zEUs@YfW)8#5^Y-US->MyL=wZj zIFZr~t}KKIipm67sl$uvihRc!f6cm4zYX^4=I_1d{@k{aVd8;iuDx0D9>XD7oOp2Q zsIQ~0Zf~+}(p?KN;BTlqj>4G}V%Gk{gqXiWNGFEI+KhyXT7SY{h*;}X;IM`it(Z}@ zq6p{MFLp*xWjjt7j?~fCE=b#j^_hi&q-(EbmIiEaA_UY^jmwpfOUA$Ys(dhF|0mrJ zUQ-`ov$2j(tLI!)@a}Jh0cV5fx5`9tKQ=Nmb-Gk4WxNg<&Z&cbR9`y!IMm1yz6K-^ zVyl6FkJ2sA5!Ymb{~i7=N$nyw0E>vyg&)8{afs5RP6(7$1w35FbLsybayi=_SY?Q4 zWu6=}aSH1||3JE|g+R`rrZS2(CmZCNNjX%k*=zISw}T*nTIqk&orco(!<82UcMChe zuKABJ0ff3R23JAYWH&57wyH+lNGAjCLo`I;R?61-Mh}-@(kE)0$y%v1<(b4GGe^2i>)ATlHQzx}Td`uEZD9$aNpP6kooo$3*FFyL;Rwqs5@W z*joty` zJ|>TCCTRo*q;7Zl@1iQdF#}PQ9aCP^-|f7$>@u-JiX|ysf}XUR(Jz5aN8R$V@=_li zQLuZS^g)HVv|O1*GB4d=AFz?F;a!G53;T>W z{r!|O-LjxyvX%GlA_*v5?aC(T_YEG990S$@jAN@`<%b%UOw#av8G6BWm;2j0H@wiy zc9Jd{0rkpBVmX@V<~(Tq^6ufmb4~wc(;oTd7}|Apid~YA{&>_Y=JL9RB6n6ssC93P zSz}d^=Ct@xZD(hXp3pMb*0@0BIwp!y>)(3TCR&!Ugag@^r&U*PI9&?T0zjZEG+&h+ zrF8r}2W}5l$VpJVzV$dxqG>&b@G3Zpfg5WVszwr{U0D$AhRQJaYwM0%~!0FFtkDID|V)5tBNRewNDg|S;8() z_rx^{pMqa#I8j%jD@!yGsb`Ft<-|asJ zQ(|>dQWP@ehc8lXJk#^@2P+OmQ<>`{BxdPW0?k-{-bdfily88h?8*?6-2=Vx$t_OZ&<`c!vXmiKG>@> z7QWQ4h_!f|{vg|&ep6syN14A&dV*QO|DG1hj=VONSwyzf1uhqI#>?sOe-cLHL&30= z2SUVvg@+~+z`i_v2G2X+tq#K-vY&cExDxfQ^^h#RQS&$VGLE9_@%@Yu(&a~C4Y@Bb zzwt68$kTp5=j)3OF9G(J%h@MSbP)W|F|yXQ5K@xHi#J{KObtRvY<5#Uagx zl+gyrgW2df&P*It4F+VKe34X%y>W`G%c^F(g|*0)$Yad)IoU7B7y;=*XMgnCt;&Y_ zIcm6p$sZ*SeyF^+KvvO@LbQWE{}2g5Fr~h_!%`icW^eoII=av4RhjHMY--nA(R|l& zSO0HZBz}n~8PUzFw_0_e_7HI&KJelN({f4po*A5Tqjj#%mT4FaqVCD%z!}V(>(V-d zTA#Yam4?wj>0tqDq+xStaWlic=w!Yd=)b_UfAYqf&1?^ zyYedqME!)_{~2cpEA?#mS97H&yMxhcT2pc5qKzhLo)iz_MbJrP>I=UE&Lq*R-0N^7 zZhJVif?}B|KMlV7}r;rr&Ex$XS#Q`!@ac&P7bm8z!GG1I?x7%B8-!h==j& z$vTTE-wCYK51KxuvS;6^*usr;TX`=bdNwnxigk4sbg_$#5=5oXvM+iL9-C>T(7xNK zXa*&-xZK5>kd3&GQ2e;!qFTb4$`2CPA*6$`NzO$5N_`0Ue3@|5e?Og)vsZ#FkYj@S zaP1PqsWC*tB+3}M-&7Xo6-V_iKE;CO#GCL{Yo}hQcwvOGeN10{a_~JB@~<}3bh&=H zSY(AvIw1U$L||ToHnoV&?{KuWe4Y7KXG#~skuK#y)7lS10&_mos{}}M!!V_GyX?nV z4HIDx;#H76nt|jC>E#cX2h9%pTA2^QpD1>;+8A7$O|~~i=&?xUU%wf*qJm5}(qaoo z=*?uh7mQA_yBg)dm{g^wCyx<^eVX)Ez8@wIrb2(>la7~_ z!d4I)X-e-}bZUQz{P=T{(#)C`u(On#Jr^0#ZPg_za43 z^CWgpP#J3qE6%&Ks3wHT%-D@Of}dWVz@t^H#{x%V1H@vjP3qIAKfr!4@QY?c@HNc% z$HQN>;1w1+J9j7K7>MGQ|MielLt^sel46kZ#HsLd&S&Ulc;p4(TwIf}2vwZxaze&J zByDsuXc<4X>MBCf%a!~P{7Wf|MdL7^A>&){w%Da$D8S6-?d!(GyZ2a`OkzIXe0ipc zjxxDqA6}mkebiRmfKDSujJSUis(Qd{VK)CcAfo1{6up?{m;2^aV~6;9?Ngf2EByKR zL#nvJa`I!m&v}@8KVCQ8-fHrb#{}TU9ximm&W`cJ zH}I_srk5NAlohBZ?h`~_#?#*m!Qkh%8LYcRc?0u6ETN-}v=Ik|KN`Kq|8|=kO*)gC zS~I5qyE32w>im2Q;`e=d_E}z<&LBeoa%rfmNwkQ!zv#I&oo~{LBeiNCT=qjqsMvIy zyKFKxaMukUvFe0S$0O^7SH6D%Wp`o144s$@6NRbPH~4~=XS*|IAeogn%Fbf4oH^6V zfi~D6@EYK0VaQ6NNll7HjdT|pyxC;@Tmo54ziVcxv1{|@CO}UW(<*)Q!qHe(s}|BUIE-R zge5Lt5+%ZhHiRf+OAeCk=MsL=^$C7+abCvwyKtK*W~1WdrO427`^VRnQ)_5gru>#T zFuU%un=o-rNOZDAYp!Hp45nOP^#DZ$HMnr!*7!eBDLhHR^1QlJ6g>X4W)M$4m{8qe z+CG)DClTLiK$)6FLf%;v%;M`7hJ7`tfYQb%#>6W~Qbx7#vN_~WyG0%kmN1)$HW)(gY(rj0|LKz7F-o0L3i z{zg&*^rvPiSbAyHyPhXh697BiVHv9ZKOR2Bt2z>Zl=60frTH8b{b_$la@6VzvtY}j zpPyQ1SNim1_=JZzU6+jr!Wp0ylh*381-U}=l(XE5+mH<@V6}qcu8Gm}jW^rmB6E5V zvj56-jr0zY`Szy%MQ&c^eNPbZH)NIG++CNkzW_UFi@LhZkkazzwwK$SPGmKVz6d zV-1~kyuCYmctsG)82vRSr@`G|e;w$xKQg5IfaQj#LTBG}F*J1tpec*CAO^Hm0dbeT zY=;_&0SUC;e&8VQEl~K~45+UbNEu7m=9t74O=7o+l#za>?a)8`argg-IeqnMe=k@N zO{I6Rt%~-xDcLA7X#_A_!4z^2iS&W%yteECH_~3&@A{NJxD)ahQmxnyLG5+ z1-t-|jJB2eFk-?XLhAG$dA-Ag+16)#!zIc~-NOsyy}#Ir&jwXi9xoP-xATRvH_-nq0 zPOSr)p`YHcPe2rp-0WX@JM$NQRe||>C${YD;kNe&r5qz|GN9dMi4iRHwd8oDP}9(g z(je*SOPs*4QQOJXXb+L;jX{T&3{^KUz2w$!)+Z8!PxtxfaT6*Y5dSsWmYxT|BF%K8 zYdnMNThn4)a42wKJI#)nze2&(M!P;J>u))=0Pho#T8+@nME7L%)N5~r@jfO}4Roy+ z-`wpoL4%-iYXGuUD3t^9fv+Fu^h>#8pN1Ak2hyNJVCcMtvVY)TI^&;$*)#oC=;5JD z<~H<3G%^*uwcz**U zJw$j>)hpzqFTmaPDZ&naq_1m1`P#PAafKV#(6RqD!SwAiH-)?`*g2hAcH|S5WZ;ItUA%-m$>)U%x%+!@)euBIJmzQc ztI5oHqC-{>m;iYRyZQ2}K2(Y7M7peb;1kErG>;lkjR@mgms=F)Vc|ddDkDZ=DC{u7 z5(3VssrSU!fKP>S7#8I{x}lwOpwPQ?tqGts`7Gsz3V`Y-Fv$dYS?$zECGygf7B_AS zYQ?j@)9Ybgln%OG=}jN<6!zdE!Q|q|jA@2c^KE z#h+9#*u{UJr9T|NiP}L6{e@2LqMWmMizWHQ>h>#zR9F~3bvdDp2)uPm--zcu$F_5v zD48@^G4p}W#tD^)T1DMt(awK-oxd$L3rSmEG_aN8j*A7*qY$3cgDiIvHPGAixXDuN z>L;V`eLS!GU!qQ<(Z->vySOyLgBOsQ=rF53cj~Zxhx-6v*zHssYo(!*Pj99bW*81j zDg39NnrrS^E}bo@b?>e*+x4JblW@|@Fp(0DxO%h{&6Q6B2yXj?<@ej%yWL2P5SQIK z=_ad&V?r?qRZZh$#o7^TpAPVWvndJSxVH;vH_+>q#E`eVey`zc~0iDKvays+@7) z8BV?mm~h=NieUTqxT>(qIH?JuOvyg09ESJ}05w3$zo0Q6`kdUTc0gj<7tdo4;>)*V z@)j69DD4M)Bs%6l#*CG++jTarLq;~f`+8{EzaPIIoyiF*BINN$wmW`)%pR!J`r|Xf zOTqF^Huiu3J3z$0Hw}v9pv?~VK}XR?03>6FU1Ug_=0rFAX{$5;) z5F4!`(5^~!Vquvey#y7jXb@OXXQIIeHeET7x|#eWT_Uh;#V+tygBB;|Jph%){p%y1 z4@00*p{sAWlb!Jg*{E~Nisw$9=*}wc5u~xmt(}!F>lcGC21^8f$ZoxR(x(ROWy(Wy zlZM*7f{OL5_{=_vHued#9EGYPhC|Y28s(-=s|fTRR4X-HJt!Ipj$je^BKY<-h z=`O!|X(E%FK8$(vqx{UsjxrnKkjN?7De&I-LhhhgPuje!@d>h=lCUB&D)BURMnlud za5rtHb{x>7OxlkPmZ(Ww(OLe##b$KLPIL8ON=)4Rf?2HraV93s9O1|#IXJ!b*JRxd zdE5BkUut;~99_?lQ0_B{oR;eT_6iJHdW0YjyeFH-}pg@2AS7Ohs2 zkReQnX)46ZluzM9VXDK9DBY7cw>9YWI$)}B@XQJE1>r@AJA-e3{=zQ>H|APp>c3)Q zyEM3~ExWr->mUhCNcnA@wa&}1ouXAM?Myti5aenX(RsdQbb_Fp2XLewYyjkQ?z$KI z82er|kWUq)xb_sOh@M`9cY4%pbFt|4x z`FB$}QsBH>tBI!+FOP{sM&Aq}sJi#dWtJohd`i6(<0dad06c)rTZ^>!Eg^5+Jv3zF zD@|vKw*~8*j>skEmc^%gPaWZ`V_i$2xR9igxfq`l-X?i7SIHTPk95B6j^$KI zC7gGR%b35Gr!(XekzVH{)aA9(pJYo55`pLkW=CAk5D@!qy>X8gku5P0>dS0=!d6D) zX?vj`kK)d$3&{H~UOb%`71i=@eW`as3y9?x_5B-n36Wne4&b)I?QS_j#36;DL@=Fp z0oFLtN1wR%!yiQHMtPo3Ae7$j!I0^L$?>l+ByeIOk$GVK=ToYm)M@#!I;st7?{Pg5 z@)GHPZz501*fDx-qIv-8JVFou%W%=ae7nsM68%>j-lg&k)2LYY#(oM4eQG^?1`N!k z1o$-)3(cLE>nau>MQUUz%+Y{ZZsx&jFDI}(E?N%0tq<>#UETR2o^=uNKoOK|xqT$hgofkccvuDtkkqR6RHLbYR_DFQSK0pUZFCGDAbsu`6O8;5a&lM}I_L?~o3wKQ zlQYjbVKqP5ljw!O`;+zB0cxiv4=hTK~4`zu(o$1f}rv$12-( z5vV=EioXRzAZrq{o{$XRMAc2S{Aut49jGoiqg>8e zbMB!M6ps8;D2=SydX*b9(v7^bi1;hUaHRqv38=(06u<|{>^O~>gG>&Pr)C3X;=}_Y z`zg>w2VjFMiyb~wD3U}MPNKs)s_^ac)~y<9bZb^FXb`pqSnr&10Rw*3OxuE+a`5SO zKPB$wc~laFEehP$>Ge5>)MHR^rB<0Bg-it9tr8qs`A!!kg0WTxe08ywg;qxL(1yH_ z&VzK#6FO!jwtvw1O@CP144=D}h}%)|G%5VgX@hBan7mB;b`ws!5Q_^umGGEU@(m9I znJswX+L^VjV-0$w##Syll*tdCa<;gXzhXj;(1#GM29d#uNvm|dKr6DbQ& z4RG){l=!LgY4kBpq8eN4+v$net2RM0i^2WDgNsVjddtH+z(~ksvPljuMv0#}8_LQs zP~Wjs9GIikVmfJI z0dsR4^>pNiV$8-b-4h-jwax>ojRlPDXr}D(;s%C}Py;?aX73r>sbqxDIL_9G$1ORR zO=19*nFjU6{B-Asg*k9gb0>OZU6utq==hs>W5IP}e^!>D_L4sl;=l7oZtl&?bq7K) z1{MleBemteKcvee_GJP<(0*wx(Qc8*=J4m?KxJmC!Td)Lzm8go_ds8F-29P+l0wJa z1GX`u6io(Lj{$`*A_{q$n{h0Z^#r8E+=3A4Q?Gv8Y2U_;?P_NFH-&-WacEW)SJ*eZ z%lG28a7##Znah08*B1dRd-qCLMY~&`(w%CSdpr>4(H_2H8@xO|sz6vFbt~c( zoL%gavbc2NQOLM|FJWDqQ`xvPUK{`^_G(?zqJ8F;ddkmw)y#Jr}={$NRan?nfBg57K;M_cmtfz!KwZLs44 zmGoa_H;{xNn6en4sj^R|w~BRkdB#?CorJ17gZ+k)KZQ{BPu73hX)9dI***TKoU6c5AGt0*gQ!!1#?7zWFOLm|x^!kg$m-SsJ!hLP?Fuz~i{p;s(+(jhN>7aM#(jb!`3LN5T@2v4jL zBX)jGj`PzKm zEVW}OF+7wa+Crn?2%RYWv`M%Y!bLhXjvvAaU3OrRZu!0D5z+Z*KY^e}sDBYSI7-6r_Q<5)4Z6zBuu29c??iBX=%^Vlcu*F zSZ!7h`-CO1RCIUPk&j$4zbixAwjC&#-)iZCQLBMYDraTuTRnRbwhRu!IulIif6O&c zd*s-}ckiZ&s+bZ1E2shT;U_jgL~D*&-Ujb1FA}w=i~U&9k9#Z8TK$Pvb7=&}(zq$V zL9pBd`N*GyklbN2VmoG#i-7=?620*D1vXc)7{h^=bIy+YI08V~|x9 z9Mi##REth}w$5ydFgjPi<{kqip2aR;pqJ)huazdxDYR09CXim~87!5m+SXaxCkpR_ zJeW%tx;zWUl}a=0$-kY&0W2=n0^CFV*z12#G-X zn05bTE=d`@rN1PgI+^x%3e#~bZNx@@tOgE|gGVx+aKL%q1ZRt=nS;@jg}p=`H7Mg{ z`1BxM9v&M|dJ#eG2ngqI7u>*jUkJ03Ittc2^2)4A71@KTG5lG)U*AX z89|apd_E7H&BV#fp%N;>{<0Mh{}1^^esVh5w$Hh#J+&?7R3^h_jaixPE%2~7RhSHS zWW~Ji`bvo)4pgTR;wEMD`YY}S+&wL8<;C%(hdJoloAj5*S*b`JxNeG z2BXt|q@VIpQ7Z>r1K*lHIRfNxY&Otyk#+;gro=xou`$*ewazetsc?6n28MGl$WY!* z{m2HEu6%+?ZzCDhSGTMA<11;J_)Q0hhy>4jK|4KDGC+g2p2Lirkj0Df$Ih4Sht^Y| zOnc9OE4#gn$m_P=WVc@fYKsL(^SXqo0Zwq7KQq$5z3XB?1!-9(LaY$MehFQW)GeI+ zZJ@yGL4ouaELyJ}po9wlX?J<975UMCpLgc~F{c~5MQEeoFk1S5*;u&Txr%loec}BP zpr#Yx4d0gP^we_$RTY0+n3>3XvBz&L}Cf7{x6 z%DEaw_~2{$Furinq{l=Y%^jNlwwRK1|L>(033)-_Yp!3}HyB<7L_Zi=Z14QN^Qx?x z{Hta-s^*t6MQ%Hl;s1kamp^Ss7OzNII`6B;r&{?NDeQtKD z?i`)yR{ExVVkuhK!H_liP91YD%euV-#Yp?&B&U`T!M4lN{hswWp`H6f!p4~_C{JM!{yWi6l8$nXG0D6*BsO>HJNiPa#8_i?nKsd<4HBDuaW5SWx zcy~b0G*t{%04}qrESX=8^v%_@KScLH`4~Gzuzfv0)PX!0Rr+81b!z73w3%beh8JCp zugYw3wD!+2?hw%ls>Br1K)h>xk(J{Yz?mraZBr_I1A=OB#oi&gTy9M#+{026Xd={D zGxfba_l{?Pi(}`aYqVB6tHv=}Dq$>J)KR8C4v^+j{~ksV){a=`b&>v7RI{uDUkkIV zs(+**jDml{WD5~(I)7rd+&VaYr?75~G0%MkvPic}x1Hg)+e!e#pwswgHE{+`Elt_y z^;;ufu-%1OR;)GLZPOZ~pXbdOm6LuzqkvW*Ub^D*xd)9rcvt1x^^ZktL)Q&;b$zV) zS&-c$xUz$in&G5rY2ANR-4qAtuh3E-iLDqA^owf^VUBS?NlxnI_rvb8(3V=P<3l}c zw|Zc>@^o2DqMN)GBcX*w@;azx%Gee=K;0v^Z=Jo^7r+wNjHao)>!*}oy zdvR{I2mhY&`o!YpN`w5R@3V-`awiKQ!+hZdPc+p8`4vV#R3KX(5Bq~eex}m#zmln# zAqbzniYY4;{w^+miVdO@OAWzpOHHcd7Fr9M^2GMi0Y|>8W%&ao(2vI%R*SE;qS|F( zV!?L+sqYv!=0md*-w9w7xPmyS^^XTkECWGdLj+`N~K!yU<_l=^s zwEdO@^U{?QMY9D5b#wLy`8XfNn{?yUUcbMPiHdoP%l<561XPac*w2UQjsyCCnSR4` z2cY4e4OKVxs}R6?-bo1(JU_zDL9VV&*+J z3psy)06}O)&?r}t=*kE3X7|z5V)t-SsLPUQ7#T-(&Q`}QUZnt~45!kOCpHlGC`NPU zchJC8>oW`T?yBZ|p~8=f7W{CbumdSy5*=&rv`c=BG(*@6rFF=FlzzdEYD} z5?S;DCbWNOAzg)g+*{A3xXrX!ml-ZzlTnZYDu}CD$7>%sW26-k)E?@k@eB%Kcl9au z9XL8Z6Rn;H_`fvnarZOk7of`gnX`a?!bfOAfCh{r{gM8F7cTU@jTc^RAg`zg){3=` z=OfDW(gBF~NDx|(&t1B~)UtgtSjR*bE^`Z|+$nmOcfkv@B<6a~mHBQh_$nX*r;>qa9YIC1qOl%EwPb0WlNX;cR!*iK9rZ$ke?O@!(I_5B01e@(dl?!n-u#+ zn+40Yz%t)N;?Bwh&m!*bC&#};4&_ii>uk%5II@rs{Q{GLh5C)qWI22~6^uign&Y8R zyG`FVEM*(ITC{;%_AuxhM#;xVLAB%zB!R!?9MZOLUZAt(01^b^P-KrEt4b6Gnt6eA z!kFvmh@@qDBiAid>kTtjQZ7lZ?4btvC@1G+F>GXajQg`79ICu;u!D6?1d=ySB`MIR z{;F_N^3Xq{8lP(*C`e(G84@SuBq}p_e{LpuH+d*4pf|1_0>`JvP=FXdNFEPevzU&u6(LSc1+ z(b#ta$ZBAECgciNri8z;P23Vf;}L9~;J2rYjNj_Vf7gLSCw-1Wr5gihaN(ZbH`mRyjV$ZG(ii$Qz{?rytkzgzG{N2bY)Y>&EMCz19H;0(oR~hF@by2FO zDhi5Q(swky&4&)qXP^=kjDgBQ#&tW%*OIz|ouXylKbP?uUgWKu%aBo9 zDNyQ98qi+)o3vv6kvjKMfvw#oF{$aZ6-?ARfQSV65SVnlZ{>vH$Z;~wsqBLI9uYGI zG;+jn1AJH?#GyyH`OT@s>HXdAByA_rR-f-GofI$k^5dY?ft~z`lNU->0so;w(tMY> z_F1*AjSwl)l3t)QT|ek9bm%Qbk}{aaS)yCk7g4ntL$6udO37^m2s3d(=XBI3yW@)A zljdpi)+E0eOn0M1my4QJ_?du$;=kF^9Eh+V9p45sO*+|2xu1`%7*R|;d{Go6Mr3to z!f-Fv&WvzxFOvfjY=K-ZqImyKVWX*Ig|sVX+BRi7@(8|zxY?bMbL>`L#o{y4-J^u> zrQo`EI4W@1Qh)Mn)MqQ*M_NEz&3P~s2p>A*v$W>%XFEYc9=!MXniwPC_lAslRr#@7 z?q$}%+D&3%@#Ql}v)IQ*F+7WvJ$`)``z)bY$tbO<`89j)t(AMBtD(9O-#I)OzkP`` z{-l7V++0?~LT7R}oB=NcZZ@wuMACC$x1*e9{h|L1)EaeGCL$^;d|$Q|k#1584Y0Do zbD{01!kCh4;2#2Ftyc>|2XCjqFL&EI;n7d5e<(=U#uk-6y-pTMJo-9ncECoaZO98cH+&O~X#&uprJJ}2IJtO_C z9ON>}y&j_a{TNrI{h9NyN*8hiEpZ+1 zxLpXM{8JhuCLiBo+gAl@lF`05>Yh&b5}vu$eL_AjNit)oRm}n@(#tXXh@9=ai7+m@ zICe$)x=4p%lr=;1V`tDY7NP8-K8ezX(`Ch}Su`PhBz%3*s3eO&K%?appMrt@(bg7| zd}aFhkc-ZVO>O`Ss5jsL-sWN1i!Qq(6$q^Pq}7NPrAU-y_K%9 z)_7#B9Qz{$+wwA9&NS19dR?H0SZ#bvm||K1C5~d$l~wAu_ii&0{OXk(RoP&z(yG8g zaa(Be!29&j%T(+C)2_n|b69Ccloy^iWmh*D4Zs5ML!2FWWp7mTTU>bDklW`ljEWgr z+?1%bvgXKqfbJbSC@8t@t*9|sVMXgL*=oVdW&X=DOb|xk*M#*(Ao?sd3>Ue(dqIPh zRQ!h(u`**Dc@TmxJ@&3h>IO|F+KPSS_OR1ydkr_-7PR}|GT=q7g%>TapC%KP`Q$@& zmuCK7B4XhkaIE@^X3IJv*8~=3IGQr111n7m8S~WHav9rtoffIy=bX=5M1LO_zwfI? zF(K92JP%rYnoF79Qm-V$Z~2LN$J|i1c==qL(aW82#kM#U=D=>u|5@%Ot1z3ZxYw>r!C=yGD4Q`TO&^)yRK&I8x7Gx z%CEC*#?^&zQ`{C+8&~>GGIVC-!B4-;-ZH5&Hn`_KwHbb5<$RV=a#sE+i|ecbKKSp- ztBk0xhUvT0x!TmIQF)lQI@w$1%T{lu>Y}aL!f`><0X=tn(ENr?ejhVP=RO>bNOWR= zuzEn$t9# zLP^i)ms6ky&5b6Q3ANk_+o7jTB&z_f(=wC+rtTClSj29ervem<#5AkRbCk6 zfLA5Jz*0B^Oq^r`ShN#Z=c7E3vo_t#m%?-iC z;A+{&)a5LW0{walkNYVIchBtRiw9M7L@T!zw3_M9>(ri8$BMwEj?FmsV*vbZsx0BE-Ng<~D%@C?0Q`Ld2bw*WyV~t?HzAU50KE{ABUi4jm}Ac=RsqNdIQF1!WrNei~=>(Asj! zcXSGl@n9Zw;HVbseBuN`xtaeyw9vx(_ALwzyYBS%Nv)@CSUP^H)Rr(|q0nKED_sQP zJ|NoTClfK)QC3Q7Kz)N@={cK)AT9@N0(vflF&{9YY;0CA3bP_kg(b7ViIk!k5EtV$0jo{g;H0a=Mg9d|p|##ZeZ}yfqUbTeN$2)gRs4|wvsz*Bg_AJd zr+$+UEK;)lyZiS>dGb;O5$4YD$0kqvp?kV4L>rW&Z?$1z^HRCL+y&8>QZn>eGbF6{$FnnsJe zN0S`UYUfdQ*)yuK^$yul0hzWyj=1W{>Rp0{6z$a~w@^+q_u-US^ea;yZsF`A>s zDb2>Egsze_v^Fm6<=G;G$Yv61<(6g!gZ39s*Ygt%hB8qVU6zEiL@`7;=E^ zBi?T|@u8+45x6g=r;o9$M?SfT;yn-Vd? zXW%51+sKVn6L??n5^Y4H?|n>{$yl^t)8>ahUxEU?cPlWb4nqx+9V5*Dt?Ui`;>k23 z4@_2KauM*u9u)$xTk;d_&->PqYAT}kUKlpCFMB}aS}-G@HsNDcSzd8inVC|j^|%zK zr9aNlbfrST#2GeD3*~MsSXz-JwU%!_5!1y%AOxCVTYjz@%Ki8o7;3OG6fD$w+7kkM z!B3GN`r^1aX_&>USzAits7-%rNUacL%cAa#o^8#*)j7cMod(UE0w60y)lRuNk%~k3 z_eFYmT%o+_N-`9wE)h6$Cr3T_MhA^+)n7X?lLvMIH>V-OS>d(QaZkGwasWp%{uHk~ zo{0WbwrKPT|8I_=L5RVckPx0uDuabxvKn0dumrohK`ezevJGw$=2i8>e2;%l)T~E1 z%|8j6XIo5tf8yZ0OSK`FAurtyl9;8=s8nHfP*JWok{y$0(^sn8>T4hZ?57p5c$s?; zT+(D9>P`0g;2TJNVO%L(;kXz@bp5a{ve@}99}Xy`XV{8LcKS9O(X_}E6EXcl57Lhs zw$Fuul=J;I29qSL2{lCQ;Egb7l^}5vMqj(edW{EJO=aL-qua#R$c|kEFih3em)(&w z@bdg!DLWwQ&`Ov%{zMUtqqIg3sf4Xy7P1T%<=Jm$5SQ~gDzCn)?DLkD*zW!qbBt?` z8N*FSI;)_6_524Lopl9^(!ng|HXoypk+8oP0g_}=JMh~4*uM9GyoJSiH3ax?OeUJ) zXzyC1jy#`6^=soAlKX#Z1NUGv;H$0nMDlz37Oj@6pXYH}RD8dh8n(l*GUOxIN~moLd5-Svf*d(RVIdM8yfdHixV zBqnL(v?w)&!{GoS2CBtLg3xS4L-)S0b|i-)MO79vD-01W?kg3u&`6)%nV*b01G~1+ z&Hm*l5aOB=TULjzk+YF4i_=`lYCx63snlsGU8!D~`za=Uh70^o@c>g}qT226- zZt4f9!9C^t-;mj$I)yRQwK@lMUYg!!(;|hi>1)q%rJ_#az~i#wYOK$vWf5I83mS=P zAnN!r9Y@Z%RL$5-)H<^P3GVOIZ2F|{4vi7mLG?=)nprpWCfZ_+rZlE(hZc@?x}#bm z6^{7(9h?Sm9>z=gup(}HtQ0Sm5fQRk2apy#0}^_AIgk6ZPtJ`;M^dsfd=|U;+U9Rc z)q3C#{Cpx6B-kutRMTU)D%ack*BQGL2N*;QoL|4DIz?^X=b}M{GA;#pag9I4kx|Vm zNUULP+MADTv*kn>mFd`wfQ{YpvFwnij$0o6CD|@dP2D4V=nmu#GyZ_)-!m2dT zL9V8*LMiEe9OW^$Cz+j#k^$tsxicWIb9cQ`}4)FgQ8(?F(p7WqtOPjGF@&e3{RK}uA zEe&2gyQYbxFOnrb$Ovw}z4|5hzg(%GW(9}&ox)8#l{i_+hx@I&2&=bG1y^R3^N#@4 zIfml2rZK>!fIS{}gF(>y>+*tNjxAi#pyzQ21k|G0yP1oU1Qf$_-E3)ENZ&wu$g$-N zY$GE-2on0;m>Qd~s?;n9FM!Q5%s0meex5i;*%gWymTtOL+B@%Z{E!~9nv0saIst=g zB(iAQZ-;x3T@u2MKF-q%}L1_bw7{dprJ;L*T9~83e_ zYN+{>j!yWmERzB59Fm>6HLY8>@q2+Lm=D&PZ#3{)IG}wSG46Xi+px}=qQlU9^=O1v ztWhIuQ1y+Y5`79WxKJ%t+>xGC3#-JouHUx#z<7@D5>=;^1$414U|pLPj}(bf{vvzH9qitF zzV*I9a&>R9GH?*Yh^*W`iztLA44)BezSPYAi)-$L;KOa4V!cd^v(V3BUbvw@2}(hf z9cKf(_&0{{?7>gkoM_;kaoF$t^4Lmfz+mJJ>vx86yd}-)Onye^sm;xgudJF^g9}a9 z(wSR?zKWN+K6ivVN#N1h{p_fS6g!vp!_Su8Gh1YojCW~Ex@5*Z5iMIMy+YJgAroT5 zd}AB5;}!s#8)P^ZSB4lUigNAZj_mQ5b^qiGO6McN#nporu{E@p(L@pOAQSce!!RG2z#TBNgjmj;`!%50w=00Z#@!7McxCF0(XJ$rYtCP1La)Hj)iD^f?EdXjp>Jcb21g3Y5OTB84jzk(kqFPsUn`PbP_?-x}y0>c|F0X zMKXP3AvafA9$##u-g5P?j4jrz(ZTUJ(vctTMW{+lP5C zaEK;rY?ZjhRftKN%N$ATh}z;zD8mLPSj!hg4aip+5HvQHsg-ZNzfo) z*J!Ri)uM^n)QkG2C)A|ZyLG(G%Fmgu91cN)7^Xse@`!8sDcxl$ib9V0@!&EYIZ)i0D zCn!taPVd*0#BuPm>3$ckUh!o?l0d5>ky>K-2P%bz+&Gi9P3QpLCN-q>en50-T&49sn>snd^IubaCc_hkXA zbYVzR&XeFXU*Ecuqhv(bg?_ui27p_AIY)&5iw9BI?W?-Y^ zSTfDc@X0jgtp3Im@iYmcKC0l#a{y-GC8(FAuk%RAVhp_yH)iuAXcO+tw37S8PvhlH zf&XB1bF0@J&6R8I?*<~x_fpQ^6M|p7(VC+Sm1YjpbMRRo|Ic$4H|lB6i7M0Um0R(uMrS$C49mz4d%cb&B8Jy|{L4<;6IL>3j1C&$W!jrig z%q$yqL{+}HX!TSr&l;>ISF`vhGcjoKlgH^Pp!&+Ra#hD}&y2+j;`oevJU!QXT{khb?sKaLg~IQQ#sd0<97eKIj`3l)pUh?%$sZq5a(~IJ#lbyR z#$jYe&ab1bVHp+2Sk|uN3ncc)SW>MGQJF*j>vCICm1Y+6GlLBfvF$c;SU140cuJ$0 z$hUGaTlX$Ju~czAm?q`;M4|pzMD{|eZM+DUN^wIc6CMz4A>_+cnqG+owLY{fJfKyN z#?fNmswZW!J7Zob

0hg`=ORUFS&j6}jE2^5Vz**xk_zhH=`S^K82XeNMe|tLJ5c zi}Qmr<^dSdmYD*b6;(wQ0!_*jHHrYW8a#ILe>+9v3{sQuSSo7~F$UpOU{*7LV?+JH z0eNCNC4=8x>~fVbNN4ane=oMW+wlHT0O?N0P~6B($NBHLYmyZ07X2#wdHmQk8N@KD z#i_W^(j?cePftD8XP+xGi902--%73fLUaZ9v zCcns)4w>+gK`0%*Zm{@hj@lEN>q=b8Bb%m+Dw4B=Q zg=x*(8l8%WecJ&68S>8k?68i#ShHYwjF5>>q9jr3pNCt*Pkn;AXpGP>R$EojZ*m(q z(4cdObH2tjs8-Epj-qrgYJ^1;(YW)sQ&r7zj;ZJz$QO$D=4zCW^vQQ$ss#X|E$mF2 z>jZmOJM%K~_sq(xGw-f&0W9QpRhi6jIC6TiS~I>->DTA0n7+TD5x}qj=7zA1r>y7) z!v7{w0Gg#*X-9AKc_bI?lBQ1Z96xy8v(kA3-3V#;Y}tW=DGrJs{Y6MdMaNji=J-=k z@@3H3t~J5T%Fw6mh%A5?sD2qb++|=RP4@P8Bj(V5aLVf;6O_Jj_%$hgeYig&aX~8jibS-)ym%vsCGbRopsg5)Hz8P6_Mejn-&edm;^k8ok)cwyC<`1dKK4WEYt=!%Wb_ z*D>3*Bgd+7*?+;Ri3M!|&EB-alv0A8kPPtteXfRi0*~MxM~ih6wKv~7oBcbmW;%aF z%)t;M**dHI<-n4R|00v*_>2l7oWxlM_E**2mT_0w0 z#!?HlJ$5Q07$*65!pad%Pe&iL&@>qpR!vb~e8X4!ABQB@CoE)$F@hTa9YHLMVQvu` zIdKquNr5*}09 znJxc$u86uhaY9Xlyr*!NqY(daXE^^}y;xSAfhmRg8(vRHKf;LC0=YA~bjNLwQm9w9T%pAqe_$y?#!fiik8W0dg&Q z4*NKv(@=4Db#Xa(Lu9qI9XtgA`_SNB81#Qyos;C?cvMqG zOh&ktYdggM`{fGH;`xAtE73yTprNB^zL$Dmp+Y1$4aw8!HtAp)~+63L&ZeC$|&qK+z-KEz6Xu=d8wZe7Ga-Cdg`Y_CfY8&BMq%*4H+NX+b>joqEq5I zeU394GqsWttQsEz#;fa1NWY08yxvKCf}3Dfd40i&dO~zE3{-#{_Zl0gKPZ)SZ;|W2 z@`*=z-8eD(qkVzjMv1TEd3@~c=rn> z{mo9S7N!_ZO&^b<0}4<|{pN+*HkMO^;!;w=DS4}F8oX0pi;D2weO3fHhyo;P+&Svv zw9F++Of8AV-ffNt{=19a_l?#InWU@?Wh&vYw{LHrt&Vd>($UkgQ%L%AF`GeuaP$!2 z#o%NZezynQ-12iv{9d!GEnILs1;5#oND1lc1)k8Twv=JQ#6gU)73BKcPq^nJULxV)_$<&?(JrK#8oRg~Dz><+`9le0^wMG}OQTJ}J z(R$!Rl^W?pVR5al0L zYCxCrwq59V)2w{uxB1oE+%P%?Z8bMD`)~UasjXN0OMe_wUvbItrVnftk>*asa+>VH z`<{$XY8&?RFx^XFmk=m$T4GsB<5?C6-$=SZSijx*LeG0aL4Ut740+lvmH#WD6Wqv9 zhAO`fap`#*-VRi#Ui{W1;LK!$i+ruC)S*XmbW5-k#Q`nW`VTY6wEWc#;TiZQ+Dv8s zF26LR*BC4U}pc+F)>UhB+dcngGIf-dA=udTFdl=Tu&B>LkE*+2X;WqU|gKZDE=NCidl`8uXqNa zu|k5?q7t6oDWwc>2w=h^)nEd)klDmLukV7+q`0^g<^ij1{ZmEEfGg(4p*~jpYUJh= zR>=fDB_Fd>%|A>Rnj7YxRXCyW{bb}M$!Ay@jMlf-0DPix<6f1!1e)Z9{CM3ioH5eT zldMLp1Yj2m%a*6WlP+E9EmVY=vV7Gnb>0B%sht*DC%MC`&SxLk^8>WG4JJF@QEQDK zHrR!+m^{l=KcB)qxNPai)+@LJkxQ-$!L_x-#h|zw7yJF1Fxez^t=iJ&$}pW~G|JFY zt+ZqqBLa5KfheCfi9X}y%bp0sL$Kh9*@SiOsHH>DqStJ}Eh-7#f7ns^n;#a@2e0gU z^vRFfE$WAOP^O-RxyG}M#$oceYq9ivx#d4g{Sj5v$S||3*;n}i!6It`GICEVV{PX2 zL4;&L1t^8{$=r=cfj}Jo= zw+1?r$*dA9I$~E=VbnolEjw3VK1+E;unXW4G(8b^U6OHGG>_VZk>kEFk*8EIf>QlF zV3T53kw71q(2$G-R{v{S@1zJ-3EOIG60#)Ib_4Mr!L*P>Gs+;6x5sW!W_$@Iz_SwP zbSK3NZ{&r+ub<9*8E7i3PVf+&k4l($>rzVjCN3(p?ZUU2T)%yRa+rc^nzF<2#Wm(4 z%osM=KvP6ZJh!>wg9wXZ@~PLzSyK2?*Ad z)p*^n${A05Xz+Gq12Y!AU^@(|NWS zv{8sJg5Y4d&&>*c4$7yB?+*h{#N&Ong?}|M07XE$zm5S&$CbF^h8eJLU&_uMMw=I; zl$g+ZFF*ZtG%_lPHTQ4Cfp1!mUW?2o~kX3US{sDZDfdZ+j}15)B=^ zn#2`f>nOa1D=%6=1)<55Ec$}(nnM{hen2YheRFM|0W&eg?fvtF0?$t*mi!@5Iu|r) z?fGZO(tas?vU$dCcm|i}o$NsVhd^ zVsOWX1&_5+n1obu(FN1YkB^>~IYtZHVa631hreIAtnlFlp7L$XIhI^8Y6Sl3-T^T2 z?q5_f^H;)7k=cT;X5Jp?-3L0>L)3OWY?l+SiF64g5?N69;SCC)bFIh~w`6Xjw@z|e zBFhONPjx2akbjL=WIM8(*iHU3EF)$_?BGz61e7zt(?8x;xs@i#<$qaVQX4H??8Kme zbQszeDF8iexuqh*1g6Yz*$B`dU$*Ms^s7mJI@=7k$Q0oNF>68Jz6uq;Wo@G*F&8-l-Sjyq>TlMH@qX~75VP^`_QUJHVAkji#&0_X2zztUy-`p&2`eZY0;bf(0Rj+&rA853eFNB-Nm{vodON2Rwn+CA0mdQ`CD{ z(1T*|LQaM*MsnY4omUGRuDclsPz=}~6Q4JXfs#wQ|&Ua`+8O%E?YderUdeIyqujS4>*rZvzt-%RjFwWCNcg_~(G&!wahKSYT z?t^ArrYL;uz(B&_?oP8z*Ymy$GgUu=$5}wMn^E0e?CMesVq^1RYbu}PCa~?*S$?_( z>vbu;?826iiTe(?xjd*%O*N{dF^SHxR%o4>nzY0QGHdqvUuCC25v}@~-Ai?*7V^9> zt)K_;?ovV9O7NDcgep1OMU=`t1U%U4;Jw*~y3WOWU9b>WTB2uoOlq56>MT9d1_R%# zZ<(l8w@;`;l7QM6k4cUG;s|QRX_4DQhF}+DDskoRCwL-mylWf_o=ytclV!>OwKpA_ z5YiE3rCG@BYgT22Yc4$q2BrKQKu8v2>4War>*PfzKm3O3F?&W^_qqzd#X#0CEqGcw zWfS%SWsZ*teW!ML5SRS(juV3R*wC1^EHlO=a!?XH%eghV(fWXAaaw@oICVZZ^iw+Q zM&E$DK@sEXr40k!l<<7nR%QVgu*T9kTu_`-CVm?UXJdGuESwch-pM6y&HboUFRX?BmFNMjpQi z&xh=YyxR2k4+C95gM-aQMJC79x7SwLF^$?M-yHew$C3TuBQF<@x6Exi%j#-1%pb~K zjPcNvB^t^qZ3zTA&xJo@np{O+gwqopkeNUN(3&EkUbJhIc*@w zB+u+J(mNp~&{mkaVG{#R9hsHN2zJF4-ztzp$1Hl%dI-B5HFA9Lrx}LvA9rEnr1Pyv zSmEi0FAc|FAOIu)3Z6)Yi6#0%&72Mk)_7@*;v415N0&=!XT2^Yu+oWTXwV1RS4zqA zQ{=EO23wo^gPsv$CwLQ{MOn#Rz)?DerpG$n_X{GQnf%^KpS_k8kanvoq4 zvdVZL|8;&x{;M6m@KK~qJ{Hao}GbGa~7o_}1$s_aD+%S2*HO!y8I9}2U*?m%&N3qPMlKP|^X<@H4jkY~5h|vtT zzl{avD;0_xr&mkKl-Ji|?DMsw`NBGk-+irla4Yb=e9LInI)0h*(kO1r*~ekIZKjVW zdWb`tXKSGeblv=I+Eh8FM^me^$-!v+y`*ITBG(%;Wkk|awSv=Om4UYsTAM@%;=gB~bL9#;I&ZGryZpp}t_?7m0|# zUDeD?^-BZ|+jaxfkWl#mEDSla519pmW+1r=Yf5QE4K%m1Sl<8=_E_c1W=#J%mkb}G| zhk^--I^BwVb{~%l3av02Nfb26`uSIdF!4YTZ!t%lZmcCQAHeO{RVR#)l5T+*hRrJ5 zptUAlM%2FiS#dS+`ARmgFe@fDQ~6sq$s>MIcVcgC*rXWJD_ix zP%Bp7(^^BJ%#qsG^kJf~Wh3p_e|1m&eP$ zKaiOmdEl=m4_}2)!aY;)AR!&1L{Txtzl>$)@KGpn7jxrNCSqhAL4uHu{QiWVIKfgj z*)Dm7GS|Wlv>G@0V-|Az>vVS#zsm(9-{R<6yj%UX0@%vm3X?`*(s;YAr>`gLK?xi? zNQvy8h`KS4N)~<6u7srd*ZnnB)3pKkb0yT&@nkZ^YCBkE(0~p-t-hCYAzLt@?RXByaq8GRF^{|!=EwgFJM#wm5KjNB=M8E zl++k8#jD2#|E>=gQpSXv9NO;XK+HjQb&sd;h*Wr3c2;nNo3N&saH3l4^=s0aO)QL; zc-U=2iJ<+Sy~U}|JbiCzpaD$+x`{2ml{yX)FnjgRGuzTEwofQKE~AIHE>{j|l0D4k z6)V=RIP3nP2%&hbJktGTRb_sJLp~$W5bZiZJpXJrMvHfG30#P~2D@(1TgwBY`*uPp z=dJkzy>osqAEwzdGx3Hx@4ec;`@0>PE}v~eEmHnI1_ElzUPh?+YB;LuX4#THSlE{% zruTtuiIOUy)BkcL!!FA4m+Dw$DVqF6<|m}>M6vR#{^RdahyGNQ?4 zv_9G43sFo2v4)T2%*h*^=-3J4uLc7Ma@%_J0@8I69t1EhpXc=T-&I@;;DQ9jI&wzE zQ-^MB=Fs!>NK#L_RD3<&Sq}~Wmm;w44w$YlhoR^c9#{YukKGni(dhswOtI41mC;L{6$ z4NHvekyjm$PXX~C4hbI!Ol2-`K0)us#+%^)UC5O;%j1fH)3(|^Mt2fqBu5}M^Sp*)vGoIcEH`f>hDx{$&XjTG8IsxNIv1WF3_ zbbE{I`_#q-fbBIf<9Hl4k66?|^>R*cN~kaNIHdL%l8eKR{!bEa`nbN7TSeQ3F&`n6 zXSZ}FX9$#B%-^{2es7}FP)5=dRx5B_e98kYLz!!@%O8xE;16_H-~KncXMs0jpZzLl zq^620%588FgL#4)CAE%+&7j9DKTeM;25%SqeFOtx#T|O-mD3NzWpKG~HPKcg-$}hk z7$n!J!Q+2d8lANbn0;;r@?=gR>*WQ7Ib80DGsV?rAK#Uj6|z;N5>U0uR`N@Dk22|B^PV^{ zS9HC-jEVXsyYp&DQBkKy$+}MLUDiicb{%_J@`w#}MOmb4-)yB#Ur+}IwzySXFM;d4 zY0{KLXaGZ!N&D$_oP4I5bcosU*Dm;Y>bkG{jZ%eRaR^B0SGFP=`V~(v*`i}mxVTY! zeo(*A5pJ?r>oW(~RUncF9AqBGr^#{A{4{nMouI~fF;gJ>_u&RB#y5tjgX_N5g?R`8 zCYq>jy_tRpJHdiYCto3N=n6fy^^vV*ctlq_Dsb*<pX%S%D9Xx}!cgFDfLmo}j5S%8lhB@@ z!m5LF)YLTBVDDGi3=qwb?%n#53f040BX(ZFokwjNSk}iWTp@)LK~$)QVxV~Kx0EDO zUaK@+d6iubt*Y2>%8}J$ff${IUo4)HE%{~2q0+|XHL}3#Y&u#?8W&QBdc*OfA}Py7 zr3f&UEt|O;F(3jpJ~vw6Ge(0@qCBAlSNnFIAK9}Kz4WUn7WoJ<;C2=&RPQ-+GxAel z(+@_kVA1J<(e@Won5o_+B+R}`(|=Mwz2B(N?8XRv=%wv18&2|j<;~^WKU$)W5(PPV zX6{rD!2#V*AJ@XDTg=q&L$&~DW^fz;)yE-od2mKtB?E`q-T?|slbnMYu+%E=PSK7Q zPcK%>zNlcMy23gfMR{na!YaF+8bq+ROy2<`D|Vew|+IML`bcRxAHd-)sQCnnq3U{=B% zDt>mC@mS9yn~?!ZnhyOBYMLW;l7Z? z>kMp4T&Rfo>-(&VAh7lU4xsq(py&0*<|;J7z9%J|irx%GxoC$o+TaV#==8SFWJhAO zM+s3*+o$!g(~uTRB<@vjlR!1T#t@{=f^U%c8Hf0s($eh`pVI959?lAAc41VY_d!#Z zy!-SP$!*Gfv$$n{jQDm?RYa9OmjoZvhhh?{a;6g)1YZ*`+c&e6&*XP-0qK2n9$Ug! z4!lUSS1Z5qd!!lGGK^#II637FX$4m8ZNaA#HHc;Rqu9fbJoG9cYcFVu--OtuFt8J< z8;kpx#PG|{JGS7J{%+K{PlOIrzROtZ#bJTXU+vz&C-A4Xs{PrL4-3hVdyXYH_QFjM zE!FL(Yl|~W=C4h%{6bg!8+-FlFSW$$uIbBWqxKH3qR$a8t@!t7G$3QRq}*8SiIMW# z{qxlUsq%FX{1xCtq=07}Zq${muu$$Mp8(BfK5C#(bVvl`D!MDURd-kMcUV>VG(}IS zYv?vIW*17s%Sz}rR(e_PMEXtBkIsxj5W+WdUnhWA+8AOPH&`mz;DY^yMyLo~g{lIE z46{5$CyW#kFZ};;%ck=(WpAu>OTfj?)HI&HBg4L#{yuQWkQ8Syk6Kgu_w&O(BqR0M zS&!C@?hwzlsZM)Xk}Dh2{bFZls>+g=H)&|k)It8Q5Oqqw7RuuS{G(-p2Czn@-~z}A zmd;HaiAqnuEAgYa7E*Fm3#sH z&QN?fdbLM=xrb`e|HWmTI< z77*}L@ztw0yFp(SvfCWeFkvNq)wzWv_mxx*%|XRXAG53G%RgK#(IlT3s=6EF1)Q4C zPCthZ0eDzyB=HR%4VO-Bdp*e_#wZ1Y^-leuKwO0)jlq=%Ct>q)KS)_b=~egAKv&z#FK!3 zb88?Vo(Ivg9bHMqVS{=RZT6e6jLq~-jjF($&p{4Lt{8AZ!PS_sR7@O|We=2W9;5>= zXdwJPw$z(&4>Xl>%&RoEg)wu1GsXdxTx(89$Wb2s>(Jipj|gzOz3;*J|HeBau{&M zx@kt#iZp$Q^G8ujoSuA-a>4s0Wcqj?0!B)#5D&6}F_%7dtT2{;T9YZ->Y;P=B>ogn zJTuZsozo`ZKmex46oSdBi04tkhh#vKL`}3&K_irLZPvO%H_(WTw4AvfvWD8M>3AS5 zu1eL|(h%z4!&^viqtU1eQ3Ed#ngJP=7Z2~> zQ$FIwjAJHl&Zd%F6zyI9?{lF&GddgsghQLD4qGRPe^3@}mm%ObRj)t-v`MY1=>pPs zPP_O)Z)nx~!TL`~#08)?F~Xjs`>Tvlb6e!u{g%$^xo59 zyz2ksu(XW9{~ziuD1m%lPR7;c%L0CY6MnqutAYi)s*oaNYT_cq-8F_ep=N`M>vz;O zk?u&EH@2ey?gF51nlb0zkblgvei7~!Uk9H&)klj_GZXJ}F}BifFnw7pGtg{B z17f4F#y(3_+MvRVn2u+Xg!Kiojq$=H`U7dNP1^FMOUI#aU|Xc-Q=pG9CU1a@pzBQ1 zSMmK<92}E4ENqn?6YVon*gW50H^CbB_^yd-5VWH0u)m@W&b7{sbQz{C(Qfna5@wgU z2-NA@(#+uSuO2X?J)!->|9SjEUgoqo+$Bp=rveKVfY8IoJ|qe5GzV`r346kZ^+ucs9^Uk4jCVr)8*m>n2Fw`N;>jPq@+h&DhqoJIju4|B;z_cyS zSyCyJDnU1nplg}6RMZ7Tce1*l_wSQmN-JsgDIi&DM@2f805%~9w%#|@Q~;yU#kWsi z?mC-S(f(Yp7htf_4*NZizZmoLPg8St!TKjR0OL7#WV}3c>lSOB^BCA}S12y5DZ z%L;kT>O=ExoKqVa3>15ZJFpi<$}}M?Pgv$nLBQ#z!Tq8iZD7XW5*TNn9Ip#+kJYxJ$kdhrI-TD*!^8M{aSUAsPaY%Tz>y$(R>G1|`Q&0i(x( z@aMrV{X+}41YIMqmkuc#(&+m)V8m0d28j(ZxEtM|zwC^lz;Uhq$n(aeW5|ev4Z^Zk zN@|UG0FpN?3ZyT%=a32?o!sh-m>==?{X5#AeyU{WdjdTLEZZeGBp0f@%$3yt;bHkU zK1XzRN>u{ifyCBdk3Xj=Ks4yd=&WRW2U8|Rb!mwziVs639Wvp?1isO)MG2ZB;Y?v| zCWt z|2zYk47?0`hlZ;51LBm+vi_v1pD_@Mcptc(y^f5yFd1`Cz?WS0G0K%T@gnT>jg z;grC&EpC9Po@P$-5+bd2T{VV&t}%pfIZ}nj-}{he~aYSLDLvg8S*P z&NLR*dwf};$MF*KZpr|mfTzoNPQgI#OBdrcOmu|l8Hxs|O}rTQY$Z7#IEBCd!4ulZ z%b>>V+W&@P1NF#3@Pl{Uh!Q15Ah*-Mx=2d&X5JUXG7%(FC0+M)$syGcnTAm_X#)+pZ<^IiHgmo+gs zeSP?IZ<$>01{@K9+8UahzP!2-VG;+)j^Xq=ZdLZM1d?HV48IYFeu!2FFxC}^egPHb z?XEYU$&_jjnZ2L>EEBaYF`d3nL4Q{HMgVl$|LJ)}Eyk*}x@-ta(GR_VcrB{P>W z(l{f;^5*oaAA0rnAAlW!pOsC;SO)B9%COtqsjgIWf7%tt5pEEvaW}Y(z=C~uK=@Z! zbwxKPIQ0}q^3|!T23_ZW@r&;ss_Zw5@yHr2s_8e7@>YLY5jU%#g1=)059Qrq$n2UXgz3 zQ<`b)uMsm16%^_q@{RBYmXVo`Ph9x89gNoyyl7<7gj~&*Ol>B_Cv0f^iaS7tll%St zZX1NuH1o+Iw75e2L4o(l>U_)$5xQCUW+z_xaG)#en8l>4?=<1X;fmH5Ucxd)$am1R zt!Sa_N8W;rk&BWQ!r=WJwP4N0HO5cE&PC=@rTiey-4YRj*fLy^C_oFuD1f{;g3^xs zbu#-4NR8I;9K$$!(RIXW??Pgzq=A@=fnIF{RXx&ui64wTl&Y|DXf=}X)arvdP(#(T z6dt>7bNbnPnyn<&AxK}rb3hK&OO3L&8iRix!wPMCvOk~^APU;C@a3}jGl_8bcJIF5 zQ>?)JCNgmz+D?s{<47H8Su2}YsVf*y`n;SRnWxhmEB4TJ=5$DUM7Xv>V%vd!I{5za z>XXQJJyv5&tGuFrBQNGE*XrRl4 z^lGMJ#=oO#Xd(p?}jQT9ym#zf^h!4SLReW)r?~#lJUq{v_{Fyw(OSvT{Lz$`s~6QSiu8 zptmurq)0vyxifELS3+sDqIIEYX(GC#LOm@OUw(poxu5{1B+~-UR%McZ;jXpv$iHg$h_h=oQ1j!a5{2x z2js#BY-Bvn3%C!w&e-VKbeE!BgKsgx;8RU);u#uukHHK7j&5NPuNE<*UOG*2(bfXV zZ>vP=C#1oxb^RTdh)ZLkstrpT?S`j=mF2lOA6yt+CKL4Q{-#=^L@J1HlgL@C_xQ3H zDFUQ+WX8F;T?M9R=ny^<R;%fyVJ-?Fv-&fpCflEQ3lGc2v*CVs8c0s6K1PjEN zM;7S?bTQaSw3UWgApl2chh0m2_Vm>D3F?GxkVshZ&S3p$k*jn@30o<1!z*^%+W`L% zF2&7TtV&cuJ+e0r(1b-e{sQ(B{oYM{ce)htcyiojurR4jJP`~F`kAJ zC518jho#nHQ)o@7!4u~e2H&FGDV6sfg^sgkb>7L4k5?D>xxwiI4Q#QV4ZnD<0pPKl zyV~n^R-fbL*$97I;T+avMjX|<4PW_k=wFZ;hbbS=bT<9=edE8)h7Nx5=_hlqhJB>Df&A^>()@`%+muC5a75MMF%-X{VFQ9c;ut#o(h2xz)K5_ zbl<_Srqrxk9!w1)tp4>B2iZb<{=&A*62(K=8srXFGr1;aZt3YJ=q{xePvk&=3rzC< z+=bCGaDwq>@#0lwPuF>krLE{_zEgQ>GkS~Hv=HVakj)keKQ&!kImPK$k=g0tS&}*9 z1Lwz1I&b3OE0($aagW2@kKHUxcuC-l<-iJc#~@aQcQJb+?mDRB_~8!^MN$gdZh2X? zHln_2bRTWgp>6EAc`POYDKzHfiVR1%V-<(@>!aD3tu>3#Tq*PQGDgq}sAK9{tLh1Y zOeun9tjtQY1qnLS+VV!dCidfRPu*EYwCoAaEx;X1b@HzCQ$9z|(6R1S$Z1xt6F*J~ zg(;tz&_-5}!okj#_a?dh<{x7?({?d@>t77q^36g>kL`g?(W9BvqnnZ&{7V1Sm%NO^ zumsM0usZ76oYkR=3<5K4_j}CPmZ3m{q_|3(f=IW5V(9MO9j1J$5%N+Y+5U_G@YtE? zwZY@11nf+^x1@Agk;D(4uI@xR4MTCCm{Js~N?o_N}4MSFwe z<1DW6gRdKt(g_!~vwjFQ5u*5EIu*Lh}f4h61Z&nf8} zYKPhbK2v!sDj#$aj9Fgkjm42~sDvCT^%9CUhfF?RO)eN&)NXa-Z(mI!uhiZasJO2v zP0+IJByvq=i9CGxrf-ssLz?9uC{po- z=G=$dTo9k$UP#y=ESjFs69QL?xd}Y6gr|*IlfOuJ?Q)%c(6vEVMa8B?h9oOb2gTsp z>DUT4IZp2~*gwu?*aLgp1l`E(S#WkUGZyCE?3B5VQ?6~lXdvdMY_my?D}V2!Ee&Fj$-(+#+r$teW}|01N>8M^F7}1TmkKJupY8c& z?|j=J^hw)&oQPj+lY;xrCjOJeGz-3pt>`nwkBO|dVnm4o5YLWsyc;VYVpa6%*$OB) z!v$53KZc5C$TdzHz;eVtlL3A^IKwRzE9vFKW5fNkE`R-LYETgykHj!b9{zl*`6D%= zdTUNk3&2$vUKF4(4+~~eC>dd2!k9Tz6675SmLabxL3tAI{nXhNU43Mhz2gpJqmQup z3$m}v{8)Tw2fIo59aM2H3T_nZ09|-K=#9gP`RUuOBY2H_4&0{Hk16FP*Cn?q3^$Kc zR#7xTNj3RxeNnm2Ux&`FNSo@3idUx>1()3iYx~PEqnnIrxIBS1%I~agF6>qn8UM?5 zSY<(`GR(Cncg%$^-;?2PJh?G(9R>b<)BD5Ozm(4Zs~3GY;{7MK9WC7O0*B^eshP8H zzSX`R@MxqN=FY7o;Y<{BR|x}ScFBx^{R+M~((jm+tF(@c@NM`QyTmI4Xl??NeUn&D zchQOIvAD)zQE$)OQ_#Fhnk0j035!8wlQSu^vgRF#;>$Z+Pct4t`VM@3?ut^Fsx1v4 zsR@D`apO?~U+jn+C;GT4)6dylPd*rFk&L4i$|%0o?t#-mG&>fYskz(|`;kTML{ws~ zP#S2=S=$9I`1o&#sJEZn-fydBN0CXrIhPi*olNujF656sxWDd-?)CO*Z=w;akZXmr z%86b60pTniFm}LZ-xC>@?H%<0LJ&Qn!9FWrB3)Ox_30ZuUEU2bB^>iaOG z{c;?7y)cb={R{7=;(HrB?+}d@EX|JEcke)tvSqJfS70_Ydz+PZF5I*+{_0(*6tAYf zEU`p8yMn55xIp?U9i#}Dm0oqoaeYo8tN*0zZVCzR|Inp|AvG1G!g*5jsmh=)`Kp`m zmDMk;I?fV6s z-L_b#S7M=)DT>`zlAU0z?dt|GBsTI)EvH`Q>IgJI{}0Udxn^xZ z`&`?rk!LXlqww>bG z%XSpquFGfa@083TEh%BCUBcx4cOw&Mll*mNkDXaAioJ&?1exTsx3atY(@t5z|YD&BTRg|DC5)^G|h> z_!$$V`sf|9ZOJ(Jm8o(F1p#SCF4DDg-Ph5!ne&c=HC<6z>5&OT1TVGQKA6E^?gWlW z*846_Hf2-XYvz{N+ zNOiQ%^KXyD!X`|9plT8pL@~xCu`&Htm<9;{_~X^zQVH};pmtb-|6(4CY3ZK8y}}T& zN%;F1p%l;nmcVDd|UE1mZ0H;I*=2Qz^=Ce$){`-L;U7xuIKojS{*ES$4KeT!n zE?jUee2mxiUm8IZ58h+F9ldYTjQSgTklTE@&=qb)F8G&jfvw0(RAP2tAubUI52ZEB zX1O@g)mHOYmAtabp*x?Fg?72iCy5cf+}&EN?f*+8<}|ef z=WFBC=~+Uz0|}Vd$!PW$cy>g=k~Yrio?eAR2?uNn#kOe0m{+&A< z>W0^y;>a-HeyjbjiOmx!CYmpLd*5@*ODxI-3&cj)pbME=dV8gR@I6W>K*PlzN~c8C_T@+3|IVu3KFe>#B1Kz7geYx;Xe#uv zDuB{()U-d4dnL6Qkx_a(>z`V}_uP~0%?}M8k9PI-#ujXi(l}P!n#)-t)#oF^L{bt| zh@Z3Pyqm`2>*OJlpL|?GOlhM6?SVn0XP_e*FXEfCw(kf4@-~3@vU!XO#d)KKoI<~e6LyHC5Fxlq;te}@ zQScq+G^a<}Brl&yAp#hd5(C3Jc5Nv?HBRN|st@ORNCG=xjh%W9lOqEq9Rge5Kks6#*U!em1^;2V!#I}z^*uBfZt*ei}KK;;X=s4PKw!bh5I~w7o{~9ncxB0LkksE9>fOA93rt*Z!Nv zK@QnQzp#F!%XBhF7(E#NN-=aN2#w^v9nwE)*6V4(!b(Z3oQonWr#t#8xPm>26drlm z-*{ETiZuiQZj@*}pz3XZGQ=O{mw7-kB1=C)iT$GpkWdFmG3>^1P)+*ZPD_$8z&twpes>wX;xANr6PXDwR65I52p zQ9Ljt>@?lW@v6EJQ~f!@RW-F^NSgYVcK2!r2&}Q?5HT^o!`=gm+w$(`>sNjfK~kTE z+?e}k3W9eY0G){H-yWfA4+qN~KA-U_L29D2=)3xX#d2lOnSZKLoz69UfJz|_jwtVsL6{0Y4 zPgQuhd2c9G1WS`M@!u0dJQIC$rBhd|KFnTFLy&TE{ihFNT|Z2Bl+xYjOF++*^DxGg zmOfMUb{9aNH6pgVxCaG^9`W=Jku`{1-+1P5j31(Vg z-dx1`n2=fOtEWmv7CL{e6tyYX!k5gMoh6sIGqZI7V#r*hJh9hn8b6ypGodVvy*z7# zo;d+H+1|Sfvk4Vrmy9~)-RkU!uXh_Q@{;xu_#5v!J+YwvKT4@xY)6Va{o|mwPrv9v z0cxOW*^}&Mn}}RVs%y35wnHGoWv9srCGPg5gkbvLvX@4oIukb7?AP{`^JVsWawMKX zr2Euw56gqPPedM%HI!|}P&ZS0_MYF+%ez#Jr?yzQ4D@)QeKbZMb%Y`PehF}_$YgNCJ+yHttkuUijsdN(f{dZ#Hr_ocfZhncv|RRh$Xk&Lg3jpJkUG%4P-(W#Si4o z1lkWJJ~7fLA86iq=OAOe09G>$=j-m@L)WpuStx`}610re7oaG*&djP1*uTlAt5nfO zg`A*A`GGPOscdENRf(oQKkbvZkfr$b8^o!D#6E|Rw-k9l^cRE8K_pk8mIJPJ%tonr znrkmVHG4G|hjK*~j@xNy8mV^g8NST7S#} zVp5Q@@&R+c(dR+})jKLT>zLnuvh&Lksy($oi{ETni3u16+x4bo%$)4e(`4h649Wj7 zbRN3M%jE5qt2Pl8yFek7K62frpf`L|o1~zS&;m<~zLA}hlUz{j3+FW2N~7{!C|N!0vWHTXqhibQvLBZ;w%Mpi z#m2>AKoV1TA>+mqS-OcSx|CVnF1c{Sh;n-Uqmr|asJclTpFXKPPt{6)pCu? z4GIXfnuetD!+rIR682SG2lt)oOmTHhlLV~3H(L$&5B{n_X2WY{U3r*nAN_Aa)u>6{ z!AV8dhNg+k`oZ0B8vT|b<=RP*Ml3cAYu*@252OiPOihZm;?$vSjMn3sXX(H#hKd#z z(5d_rg{fK|0~tJrjX6S#*oV3rqVTa(__-cB%I?WRcZlPL%YzsiMv5!&!2nka*99^2IHn~(H0B+T${utk4% z1+hHks|&IkY9uADlbQVU)mlGVFrDGkm{761LVO{h+EGJZp%0mq9&g{-)}U+5yf5!y zxRzcGi4Obwtlf)k)V(OlYIrR;9aWc49_n&DzQGlA01CKU!u3vO+FA-oTgv}?uA&WS zSeR?JYn=sB3WAv?>KAxQ(ifjK|Kfc-eYxdUoG0*+d(zy6%uEdQ`Ii0&K;3=2QzdOZ`v*;xYV!ekvv?Gafj zUw0tyGF4e~gY}ae0XTlxB*`%e`f*5>CAi=<5 zlzMn?xdQ~6p*L-w)fnJA&w&a&lR{F;Q!-XI*t|shJ^~fjoLn#MX^>)pcE%RPK*xQp zkr(FSbZj$4tv%klu*-YKt01Spr3(8IG9H%3IG3;EWP>51#P~zmdY27??}`L4{Kq?> zwm2XWhfNTNxp4%uKHKctSq3RmL0~s=b!}Tnwdm9*D2n4qK$YeeUf;wswD?pur zS@fPpdl{w8hxAsl-Pxhc_@9nN^G(>^YC-E^OX-Xsq|r8}K8v};c1r2yxmDYuO@#w( zuC~a0L;9T&1=w+z`I|tU%>{O7V;NWLiTqA)0@sXU)UBqH5H?;5Ly(?F*4^w4+nCzcQEP` z*KDLlly!>5s}yfoYvN-gYo82%hjLUhXV*|*R1Dfgsa&UTB`=Aku*c>+ zvHA3UMbiA$(8N-6AGZ6TZ?$J8_g^>25Us=p6pQ=dF0)=`;c9k|L*lG2KR62JV)TsD z+^`*kyB|{p0BOv1)D~;MGw?}dqw99Ckn7dxFSh2783&@%nsA|C3He(NCMJ}UT;R*p z6DB3vV47}!MJK4OJ#Gz$=B)}1r!!rSoTs4X9ZX3#dfhkM=@jGlAcmdTd~cjmZ(tr7 zN)-fsC7bZ}$VI+DbrmKscBN)K`?|FN3V*i$@zgY;wQw2rNX?ZBx!QFw>Q;@sr`%T5 z-O3M*Rg2Rb=w7tuxh7X-!g-Y?JEi!6z2j1;`opUbYHdd5f>RHoT_`j2*t9dhw%|7% z!4{M}t)7cRlt1xOC`9?J{aEMFf5=cM zL!^O`I@t03w5S14&+mh2Sih@R1-Gx9MH~iW zdsjY%ktDGufj`FYOXCU1ED^`v$5&g)E$~^+v@F({s}M5NwqLqS;*)d!NFrDC#mT&8 ztDULY{*R6k)Q5D3Bpfg87y9r*lZL4RO|?jsa+41# z51#5FYHm>GOK7Xqc%bLYMt0M`MjlHCZ7OKBP=VIoE`i)9m^7EIWM*=l2%pB>UsiVN zA5x~cW_RBn zQeIP-_xAh6J6!BoZH-!CK^+ChtA(L_%w%;7pLZTu*2q&0o&N%{vd7#{YnTodMupAh zx23=$BC(hXqpd)GKdc(7N90Rkg+jfTI~SnE2Q&ktRmih||HmboTTE$PeVDG=3HR6^9Q2Fn5c8eiI z%;Epi%}BBGz4+&G5_A>pX+duLYX&zEkd&NKP<1Q=P&e6nBu+n7+-E4y1 zY;ec>h<4O^%p6@hXlzChy`1YjOtl%eup=10{Yhbs7rECAqKmk)W0wIY_MJHc`uFii{ zF{=--V`!-}*WVrnzY}W_E45j;9@@o=^qWhiwiMQ?C3Ks;L_kN2H}y(KID9}>U{%o7 z#Rty^3JcbydWr@9rBzo8a{gqHOi~{N8it};pjAy~E*RdJgzEj_=CNK)kg%sjZ88pk zbYXNA`_Jn=f{EHGF`w(A3>nDg44tk5@80`L@|}*jv<{78Q+AH_Ojkr%MTO6rcv(1) zpQUxRFDwmMCdo5KL+IQwJ=b&&X}wA7h#*1SiPwKhNu^ex_(KWMZw${AEi^ol*zo5-Xn1cdCGl0utL3fVq5 zE}2INL^@NQ-$tcO(H3l=S>Z7=n>Tmub!r3*E@AO0!oV zuMm}Pz_dU<4dm$GH1Qn?2nXZPS5XZlC`^7!z>#{?p&X!8cxCv(^Cga9Ci$nvM0`Bp z+=Xg+Rz#C2{_2zPA9Rq#dIkqB%0khvKlvlCVfSB0w#YTK-KG9{dgY%+&^wKS5w}5r zz0)%~M3dF`hxdC4idnJcw5zYQ-O14%l%sfD7GofmU4!$mkeH{RSe84it7x7aJJ^=w zp0^Wcs&|dYe1X{slZ9@r=dlY_nmyxEg^FcqR&F~@r)h}qR}ns^y+Qn{9UB$Qcv0im zJs?Ik{K2zxph6nR-Tm!gNjq1iMo22icN)%djXC@xkk(A(;XYRmU?$JUBjy# zi4J{;mVF`k*mQFtkzZ1yj~&|9>Wrgk3IU_DB2l8A>I5&(I*+e~aW%36hnm1zem;cw zR|UWHRHfvFb{SI*Bz$hd&=C#(60|UkPbe7LinUAqrG<}wKykXE2vZ{bD8HpPM|}*J3NF38K6D4U&87l zJWm1zZ+^0s9#=vA6J9f=`gzU^6{4+6Ny)~5q*HmYd#;m?(rv`T{;#@fMSR_Kkqruj zZx;7yOPAT={g%`fM`(7I>7^X>{I(C z%>&6h5W>pQQm#W0hb;}bm1eL$SA@cyN5eN<)E@E~2jBfsgXO=)VqQMlYfwx1exi~w zcL9<}y7X1avC|_%l82aZqpq9C7w2b`$|Q-%cJcnq-+BFUQBD2?yFj+)hPS9Xx1f6NE~f3x=WeB7;!;QS_aYc%lz81HsQ>(X$Q}T9!sL|w3FZ}ILTr2)T5OOBro~D z0Jt&S9*QP`mk%AEphJ53!SBSBLvG16d*@ZOJddO*9NcNapfZPx1WRTK<@o2>WyVel zJ;ai3)qvf1$(m4#b9gn&TWzR##Jr!>b@TSL{H*`p5eWH#YOSjF?I+04#=y1wQ31Zm zop-4$-R0N*WTy3_%>8s?$!cc;u>51rACnS**&3=NE=A1CsOOUlfJ1sRF^m`5>zaHV zoE;&)Ijhq>y1(=Plqlg)rWnMCihhZ@@~IpJh?!vl&q(VvmpUKo9>q}LK>8NRr7Boy zgYar9lptn5-_g#q{}cO%pKfxk3AdgA(~v+8$o#ulXSmzd-xQA8C0i249uF4)-ReNh zgV@oIoUri?)enGDf}cJRJ7SMe>9@dD?_NpX^kf^XCU{T_b-b$dXoKkXqC@gr1cCJF zJPzp}pK8}Drenu6!i`1K|8!=c#KXCKzJ%0?`u}<{8bkb09vpPPxYrgl$qj0~F(Y8Y z$j1}U9TsWMPx;QuYCGms4Olclqxwlvs$O^>Gzg>ls+Z73D5>;t*GVYbvY0HeeCo=$Bo zCYxnU)!}aZ7-|qMLYrgY?3ciVHyEZWXE}rQeJD6<4;Eedz12yiS#0o>OK)t7_Sn@N zqi>KN)@L8}_m|l!ES1GUF&#Ey^zbLjxX!-ku{3qS$zU|^_8S78^>%j)BFRw>v5<9I z9m9A@9qQqebfx8&0{K5=t@ZNU9Oy~x47q7_!o%(`dly<2+~qPE9(+N%YFhE8O7Pyg zpQyvw*|;B3KjmFaid4>hEo6P9nsRrKf6bg{uf7Jl`}-+@t4K zS1S8sV`5x@A?O$Z@OIIc8C#n6OkXevJwy(o3>sKVuzuG46u$+EJ2|SY&|z)l1X<}X zbL4ieJ*T}f+7CS=#YqY+7Xbz{2r+c+D0}w*3g%;HycuO7xZw7vDM8aEB_v~s)Lf2} ze95Kj{=X0BD9moN)_2%EENzOQ;^*h>HmwqAt0qnG{)%&oUwCzF$P)bCO1mEz)t|l% zd3aWdi%V!ceS%qPXchP#IRTWN$dHgO-RNCXV4@MtO|L(7^EZ7+sSasfs-eyPi6VW3 zGZoSMl~O;Ir$jbHFjm|OHSnLOQkv&qe{UtNbhCyG(0Aa2Ta2%{IMYZ37n7_rvTT*m zS3Y=xY^{Iv6yyRHxDe!qbjiOf(>a1r5~Cw^OH{rK~_Rl}S<& zCO}TQ;cHI4QA5~|zkArOLkA`xtsQ|BT4dv`vGT#9X{ljk>fofS^BTf zet>&+0)RLZ6zPvkz)Ee@Q?eknMIlrCd4&k6j5J5IJeRFjpl)xh&>hIW+J7oy;W?|K5iZ3$y4-O31sKc?xY-cV(e#8 zPC0L_j^n>Y=k+p~nPy=MbFm74LsY}w1yIa!HAvu>K*H4GPDAS;0gpLK-uU6mlbyQ_9X*0Qb=C;A1~bF9W~NFx~mG?t^Z@gM-v(0WxsZ<{J_1R zu+(r|!Ey??Dqq2r6U1v4hrLV-+P?LP?3l89wr7~aN!LqPawUy)+n>aA{kv=}od1zc z6TWnvkj@x;&EHfeAJpBnIzRu@;VhnGyM<$77_^*#x)s=tP9n4@U~0FVxA#9OCK9&2 zK^{QednILg1$WpZcXus|MmOKTVcAMApiwZ!I(EDALF%e+2Kg`fBofso3nb8JZepqL zyB?yqnv;g&tpVTby)EU)jli+Z=@y}(ALHbf^NK|9O6s9c{eHYpof#~W?o=Xy-=)`G2Y+9|*BF0@rrIMbC41cr-C;o>!E zsD4plMF6$isd!-?`Yl0qQyC;$w|un7Xw1+=6H^@rBTP3XNV!98^zl9;jz^0^YsQ@vp?p8-3)i>WyaWE)rLT18>kt=H$|m<>&HJ)!>pF^21QN47X_` z39m7h-2ZhYsLPcS;3-2NIV-SMwzQ;d;$>+Y(lAihIHE5Wem5gC(_fNbFY+bv$6w2b zuU>@&o6A4W-!jA;Xm7d!jac-eX+>1fC4)E3 zVG7h>{q=Qk3h<_gjz8+LJ?7$>IAD~*^p)Rc^8Q1K>IV{ebz zV46hPrwa})tne9s&_<%^l%f7Ktdx_$XS+)D5bY%5SxfB^4)WAdsw#95UiC}tIf z6z36aijSnPTqw?eV=x{dikW5pz^#^>rdqm?sSZ>j!9X8-rln&? zNlv7{VoGgY#jw1o-$~p!-BeKxN#7T`=6orhnE$kb@SarP9qJtNk_+Hbo{VT?%C&V_g?=(Hbd|Lh7ARGw$i;`FbvZG7qgOdYSJ7l-cjbLwZy-w4B{@-&g- z^;ieP8PIOoViGl1CX`j~80%wXCsyN}!i8(ZzPzoO=asSD7en(P0@*(mrFWbuM)EEn zW!N#jG)I6-agzNc!%@jW^ik;*09Pf(z1nzeJT)^SOyPb5tVN^ob{J7b2M&^{H-p7x zjfSJnW;xJLwYh-QS3HQ~=PYe%nO~t|*YTEwPJ) zStpMzD)qFgN=>p6uu>k3HlQIbL)PsO`Ma56(g5+ZHYV$Uz@qm~mbX$h1*Q zeMMtcbqS^*yX#7B4G>pf;mSmx;#rmScuQh zmKMIRchPv-3}aKD#7~-9t^@lz7f%PR4S_;EEHRMmDVjsp8nD)mMd(7!Mnd6Opv@ZJ zP>+Ll#z{Sdt^QlqKufiTVn1m|Ibx~=*InBKVVEt#6nD}M-Gu`V=Wl{{G0 z70lUmNeT&JQUo`o)_#F6HTol|EROV}l3u!{;yV?H`wdJAevWYa2ooul76K&UD(imT zAJboo0!5QIt0>G(Jz0E3 z89qR>)18&8iTbIcCnx!LpXS(WZxN9W?^bM;H6S4@xyWbTWrdxzDPxJ&FDi`a{6ZmslnFWJ7}OBZg#M4 z9yLVZ*ZC&!VU2C9(gHKri34@7_OTG>Ki7_YWBg%D)LiyeO_^aWie2b}^?`Q=CL3D-=Pu%)*=PR=5%_aQsMYCXKVJC7J$|KY$}I@ z)4Q<)^RR(Xu0PKw@518VCdi0r2&ye^`D8*QVC-IFoQcl})ehMbPk(x=Cmy$JnsL*5 z=qCqRA4V!j8ZO&<;3^5>CrJi~Xz>mWMyG|FuT_l$#GMDz{K;hpU44GQX`C+#6l2k# zN}#+fG>oTCiWbSvmhz`zc#QkPa9;GCVW~5*O0cE11zb#&ppBlvhsV+I+NmwHY~5WV zhJiI;!`}(gcAs(Sd|rcv-q2s0*>ZAelun9XA@ED%by5LZ6a(;rsNstwocP-1wl;NZ zG`bpS*m}IxbiJgFHIb(-2Q3hQd*U1v38k5(u$N6znxN4aA-MaBt9m;Zr~jQ1wsUBCE0_I}cNql4vzK zxeddifAG!pz#No9549(0S=dHIZx>WZO}WjND?F@g1iakE+u@@gijqvVv~tskSI-bC z2eo1H08fWh1Ryr=?^yEHuCE^|W*>YjbzMEEynL5&d9p`>yiG#~tAB+!xVHs_`@dOD z3$EW)pW(fxhwd;IWbtCLHBTsqiQwnhL?YjAm!Ra)3DIAeXQ)$`JL)TrE0F9cCz-sC@gLgyauQzU^a%T&oe&EGxfCy{h|%eP6_4KPp}s$R5Qj`ZI;a>oQG_$aTH9XbA3vEcqqhRhU%y6gQ1wJXE#S+FG6p? zvsrDp@NMrSfaQN-nFQN0Qvq`cuIQiI&DSX$)&7VTIy&pfqz;HLJrJaC!;fOF#>iK~ zc@bEzFn6g5Uu;+=a&IQ&6Ny9f3J9fjXCs5{i)G0E>G`8#9CbwU{AF(dulVFd{|0!m zRy}Kg>8iHMCA#-e5V|$UrIwRa^iaTdI)GI`ZV44%`bOA%;buy9>B^te9rVwVd#`2y zrtKD2_D5cN$`WfNV&FaTFUoF4*Z7?O%`?V2+UzygIiV$Ds;vO*W_UpR>u)zhcnb?PatX75ne&0r=#08{!k5jZH=F79oQS>g(@7Wnz{8 zF}X+<82~Y#r)h73C;{OH+9s=j5aj z+pJ!fL3L4$Y7x>`U&Vgb0W96Aqx?y7Pil@X^^WvEN->4C>a0x+bj%5$5|M9Sj*QZcXq$^t`kCHi3~gCa}MAjA~b=Q;z@s%l`nUK8{$`l zFe=1gs3e`G6b(#86S#KWGC=ejlIYYAw+HG9E{6Zr=}v+%u)a?K3U;lE)FCU?bg#eW zx(5=k|2G^t^l4{HN!FuT$!)tCQG({c+))t|Ag?e`6AtJTim3q}C8+vK_%d(gj7QaB zMR_hQhp=k!DjfYj^v}wbIcomnnX+!KXWb)c=cSf-aZc}S#~CQZuPmzEJVvEQ6D`5f}h- zS?>$l`AK5`=n&$NGJt>P&_Q2U?mf)cV+fdxaA7*Xpjh9q{LWuyQ^_iT&;aG9%*k)b zA4msFg<%a)#LB*y;+wL{)K+Q>e~+dj`bxBO+WI9?0A~+xl!jWztO3M>x%DDMJoPAc zz-`JEYe(ga(6#_MznTXPV$^V*k%kK@65!MPNLwr~k1 zJ*Z@y%$71^tKPk6h_b_v9`&3`nI1&5dXY&rL`GTm`DWD4CgNZ87$1yY>~VDvHlnt( zLXF}l$hHbcMYB*p?f0|4{d!NH4mW&A2E8@iRoea&DpaUf*xANX!=6Jm|4C?%l3t!y zb~R#~L&s9WAyJdGA< zXuvBg&u9Igr75hj^cis!@hY(8ZF(yq?<8&3;3&*~SA|Hk!zv(HJf9otN-P^-Bs**& zEpKG{8?c654TpN`VC1}^linDV0v9=LHefGo@*p8cE^eCWJ73)IL4}#$dyO+}@Bo*d zN9WFsQGs@c%u3#1ThT?r(%HCPde?{4wJeAZ^eFv* z25f{T_ib*lpiOW}BJ}c|NG_A%fIBB@H3&bg2PG>a^tG7 z^ViGM05#bM75OzadG;7B=p#2S1mqFrsg?{~g;I?jKrehQnk`hg-MYnQ{hVd;-`n0f zL3>!#obBE?HFb0UN_uTta%Pgo@Q(R?=G~L&j@iHrxxDPPKB|``+;tsBdeR;#p0UWW zcL_S+ex7%}W$r8}Vun0SH9e7C&)W3emqR+Ka8{Qf!CX;g%Sax{DS6^x{tdBg_}ZD% z1S>W{t&Aq)hfkFCgtCbn0;;p$k3QKiPU?Usw}QNSj6lgpJep(a$fs zl+`G{+HYBa-%5spbfYi!kud$tfQ^;5QlqGew&CjCW+}fx%2+puStuI0EeisV+*4%^oCHa$fDRdXw9oNu(TO#dG*wn{j-Nfm zT4-1^@2UG3c9m8FHJGbx5sa{BfW=&=NfzdI8x-k8;Sp+ZoU`J{Ai%dmqx1C%B~y~~ zi3sr{K-COzM)cl^*%w^LUpgsg=PfOIm3!XGm2St*A`+HfiURf-5IwJ8sh^~%?&20K z-%9EWpI%PByYOUV!on3xgXB@k|lx z+)2DQd}P!e$n$E+6BT?YRcd&OWlnHy#{|gjD_8nIwTNj;Mhq}T>yoa2rq<|7P(zkWqC%&$Y!C=2rPw$PQLc zpv%d&XWh{|jCXhKDs`Y_p&e=E(w$ChPlOr?}~EN$oNPYb=h4zR8H z|NW?PU(ON5du447Qfje0g4rJdaG0QIUL7*zQpQMYn8ucsk-SMco3oC70zMZfIE)|y z$0j63jh9Z+rN>5Wg~?IqtcTmob7a9pL~xt-fso=WKoTir;d)TRjE%I8 z4(pF+uWgVQZt=Z;6tA zAlx%#B#>RH;JuGF1c(#Q+fcfNsY|ge`IKmbs3jsr!CYiuRHa89xc2*guxH(-8?EN= z6N;P^E=6>e(}}`}ybr)=h4W{i1Y`VH9+D)}pA6~6-+K6s{GM^gU{~`2U{Q9m;WQF1 z8V|;TF5#+2InkpLygpDEL}qC4fFY=>VCg_qF*pFtX2d&w{og6d)Cbz>$6CoPJ@drL z>mkOI=lebPOzNynuG*hK)x;z`xWlr{V4#;oh-(28xrN}+T$<@m!H$&#mmr`xUQU1& zeq!}R&nzc9Ic}Ss_$zCE82Pc{cG2cS*QZ5GMuAN44 z!i4Szj&AcLNGv7wZ{Gz;P6Yy|Wb(d5krkWJv;kxUPORbNWH)wcg!w~1V@R6MY9(YB z!;?i-(-ZAvze1nF!Qrn?;1je2Y9014396x*NuXwKJj9!Ov1BtEK_4KRxHH9v_q4z+ z_!9Rzi=cNHI+Hf)vs4)Y1=`>XC+4^05br~RyLok5ks z!~S;Nji{>760dQ~ac(sijR85Kje}VeZno=0WLcv%(o-HN?A@$CUA%;UC*&*<9q%l{ zMatH=6>nM&%tL($=@K2~pDM&)TR9u%{w44QxjV93@K#*uZ7v&8ijXGd=?l1_>k*}H zlSp+~`f%7&4CRtA;iXqu!l|i02kcz8CKfJ?v?&MFP-jFPv6&G1 z=8kov+Vm!;hy)RA8KfS2Q+(LCJ`mOdOrnU;0tWfZ z|J|+gVHCe3zu(C)HU0HHeV?&B0cdzewW}>Al(Zexo98-V+xrA0@r=6Y7Wcs}(HR6F zDzBVW@|4Kx=|2e*>qv&n(u`aUHQt|I6a#%u+KhQ>94SpE#SD5RqZOyA-XRoUzJq}L z#*P??GGOD}t0>939N*K~;3)P;0iN!KTV2)5miCc+mCSE4f>st+RXI?Jn=Ex+Py8$o zJqY4;9KBY4o`vF6W5Iy8JY;ZGP6$B&DNf$fJ6-uJ!NJ~(p=RRXSC))SqZn9r?+z($ z`M$(g_|?P}E!=x&A)aWS&pDJFGI2n{GOA!|aFuTArw9U4WaNQe1_0 zed)LB8IT-zved!tuRd*J4&duprJFh|8#oaE^}{5RPY`piRAfSshuU;M?Nq9Fa~jygnIYzTu7!B%hQwioLU3FZuOE*s>N38bJ}x2N zR~6hShz9KaB5U;U1QO?0bT2_knA=q}0XpP6FYoNFE`ZiESQ8(r5ls}CAEvA-q`{C~ z8UMzOQLE}N*}H;tw$HJ5+bHR6ksk4I785Sn^1p$#QyJZ>*OMUkWBR|#4!(?%gLF>| zQoPcBYI18I1bS1^Df`5b?V5Zi`soh1lA33c_!MJ)lEGbg4>Bi1m{PQ&LJnC6Xl@mK~FDQ&5{>U5fa!hdg2jyeje5y^$l#tKpT! zIY+ax_Plkk)u3}Nu)mJshqw7|yv-@o)t$jvGr6b#la^~V;kgS*B=#FTIwQ3B)Z}Ff{EQ)~u0Op)NLUi{H%fFi`fQy6Z zi0_c;8xkB_6F=lW)6ANV=gA8I;KGj8a!vtFB=x!2C2hiE2_!E|&d#P>pUZb_|q+k&qYH~G-wxSqQf z3g60R$eW3)W(mibYmtQ;CnQoO281IvoJyR+4HS%yY6~uK_8n`uD|ElbexWlYn$-<5 z*)r;E{OB$MiB|o?Wn$4{?u;w)=(Ry{4Xz-441&n*3>3(j;G-s|C=t0`p6AZ z_m{Oh^Zpl&1-w|8VJ~D$fRFc*B0g*g%mifrdrlB=LwO4zAPf?`@A1}mQ>phHyu8s) zI?TqZ=vm4uE=nfGHWzreTPJ1KU?{WG(Ic_YkhfJqgH5u4LURoEL<^E=c0}+eps=#M zflU=p15=K`eGDCQuLXnV64rk6ix4p$%oLprM<=?X zk(S+ELO1?=Prs5gT!0y#q4mz?c73zL*1r_JQZz$%luI9#?<@E*X$beD!4Kx+P(FeUR+EQnpx7vgja zZh>OnAL+yraxy(*;pyN$z3@&+{x=JZ)z~hINAB3Zu=YMeOK|Y8=sfk%U+l;e1PgHE zI;x>H5E8iI4wFzSV*hMcd+|e0YfYbc26H(A znkW!gUqCQ@aC0Ki$cd#4`tWjCT5cb370e*FsV=L*dXm1qZ_Cw31fkb&TH&Wk1F@w3 z+YjF2P}|N&00u7+0X8fa8~{(ZpsUSx$Bgx{$hyn?b)&{9WN2hjQB`@jbN`Ze)~j`p zmNKy&71=`9U~DXe^U}jn!Whdje6jh$k07;2r>EGoGdi<@Y+~h#8kJ<>P#Ak*w#Yu# zFT_m}EKiKD7-$DscONo@w&-|_S^3|@n}|zWl6b@T2vH27-R&YYT7M<@I+DmOjg&xj zgnIm(rQ4~)tU-4}|Ai=JazP=60!GV0vX?8IG+|ZlGc9vRZjLcWNhl{J`uH#P5-MXa z9BxoH4}VoNvAOImDwV#e5O|Wso)U;My&NvX-vjbWz;*CRJ0pce)WNGTPD=kw_Ix!U zBs+R;C@Qu1A_i(RCfIOV16atWQhLRA?+VOkb+(528bhi78N{`(9tzDJ$q8~D9CeU=j zgr4e$rbp!r75dh>3%T7efIsQTvT6nu&g7Wmm7148AO9p5np#_UvzONOrhIX`0h31T z#*vc=1M%Hkv@d7zswc^Ql*0)#*R<9L*J}%z*QZ3%ZxjXJ(&194?wQjuLjC$u4Z>aB znW8x~!gsEfi{SavY_rea<|g|NsM|E4c3hb-kkQGC!gDuyw8KexQu1#zafF)L@lQu? zaoyIOjDY`quc=iMF5Xl*UI=x3XvP9xBx4D0bRU@b*0TNHr`k)s|B$4EP zklkQ6%bh~!FeoVcIdFsuEIPe(__?zBbtfQm~FwN z@oI0v;~_A9HNzbHM0aw8GH|eMuL@L1`U)~*$WdYGX^l5u|CFPLV{)EmaTmL-90v{$ zX_?#B3%U0Gp?fdBx~OM%9)-IT;g=_RU12+#F?}bO3vIYo2HJ7je;K@$a|vzBrqf~> z2j5dW%{399j`ZwM9kW~PJ1L`Rd-^MMP4HSm%zr-UE1d!ckvHt$wfQqcN|pr;J}z9} zkL%)TldzE4ey<`4tqVVW4re8kHHJH?vhFZ~mF)4B_9i-x>*H>&oNLH(GFtb1wXANz zAyCy9Kxk@(TfSV5(=JiPAp|TP*$>XRZve*jt;8Dk^^9t(xooEa<0rn{Ij4z1QL##5 zS%C-M(-JEUQ@kzEY&?4%U!Opz{Vhv{awcI-_tp6&?A@TsmVzJvWPme*f}Lt8EeRG- z8BZQl4jEgAj`$%aU0^5Pul$$#xW^tg)r4+xGlNz)vc%)MO^)cXS%Kg4#Dyt1LAA` z%?A;Q{#v++nx7ihIt)h!O^Bom0AbK3tAMW-3OGuGYAqjqRZv`~S;7F|chBVUch-j% zrQrLn82VMJh{i}2B&^h+1YY}eUY_%rwn>}Y7sAL8ttfcD$&btToq)fGFxSj=6Jf5x zo)!m>ei=;M$A9!keaM-0Lq?(3K>fwk+*mu?)-2|L>k2`aASxB7B8A$}D)Qw$ zwtQNOZ4gtKL{*PuPS3K@sJaU(62x+J;N8Oocck?aEi^0Z{?)w4sEyl6s*u)T0>-g6 z_}Su|=AgrsAWp0ij~@hOW5>tmJp`DD^^WNJS)ln;UD}tP7U`QcD$m60w>G)Fo2CXjxkxNPVmg|LBo3m@ z@C{xy+1M(@T>&qz1!@L)6{H#~r)>Ed{ZiJJ%MbM%piyHK`U2JH%$gmT-Kty&JUV@seO zm=8z!X6!{f8Pq2d-1Dy)L>47+U^lS&eUZ6TdB`=eQZD8Ftw8niStZ_;?AZJlefBLZ zwY^?OFt2voZ_BemN~!JV?05fHY?FeR28y-_7j$i|#)H;}@uy9jkHwTRtO;a~7|X=T zB+<4?Am zu}>S82}cte{mdiN@fu`{t;X*wCb4O0T>|s|?3`ID2+BxMNrrPxNdg7u%x=h#MeigV z6B{Kuz*7(B3n-AK^VD1?oloi@5W{|)CA^kO!59ncBsG9cY42g_2{3X5lOqkIHD?&> zOH6Z0t!Ri=m%U^>*lYXTy^df#BM9^QqKfdUaqqu&ayF0^NZdYsn@5*!Q9v?rSed;- zZu>pt#%O7Ig?~fYl6P_0cqFI(_CA4D7XmF*TsH8&o&|o{0%8^Rs5-MP$HOV9rwkyZ ze0hAYZMZmy$J~RnYTOIE5mSrx^+eP(K^39_wNBz*MRRRV#a5?^t)6wyp&-EaVPq}Q znGM6BlZfH%gQFq#0dA$C4Ui4F>{`F~&i!x%y#smEao7YzpYb_x4=&XPW$>YURI?@F(V9%7?76_Y}F0z`^w&f_*O=L!a3_#j_e3A&HoY6fT zFL_X82Rx*sqM)db2Pg)ypHQAffK^CLuv-fzYQVh^acjdmBQ;9W3mtZ+v&{419B@Nj?;A`{{S zC4-)FdEc;D0?VwoY^EC=w?xzP5QU6R*oBMm#yQ^%<=-{zDk6eLP>l6AW-5^-AVyUI zj>Yfb-tfhKEDBDJd%Vxt%cVQn1@2gjrqo~e8N1)j4|xf{$M%6g+3_0;EP`RBYYt!o zA~?HM_rrUmuF7y+S9g<8oWZ3kEG{xV)O@4%4zJS_@nnwb=2NCF0Dc`1bduCpYkLsU zE6_a7FeJ}%dh+sGDhMC}R)l5}eM_ZPZF`JYDIft!vv87+A#eYFD(fh~OV){xi;KPB zqY>udH8%fpDy+3slhKo)zu#emqR-QI>;ho9UN`oSmX2U1)rW_={*Sa5stf`oo7TIZ zeEW!j##o-&BMOc?3HZFq!z$NJuS&dfS!1j)`m$2snPaG4xZ+BT)Zh)16?EkH*utAm zB%x$-N2frpHww*x;(ELa4P3fi!AM)CjrIKny(idwr?(rMJ4v{n??pCYKSF>nb#RB3 zk*JdM3cxNt4)lg5As!I@jLQ(jAhIlDAN+AhT9ooqcI#eC25Wwjz{y$RA;?qn^slJN zV`fCr5P8M>U6y4#zQ+L*OcZnuSPu=OceKcaM2A9xu?lf!hxUX4Wby*YpT7-Hg#G`w z?IQopE1GEJ6P^Yj;XKKbIoJCPYqo+zdIH~ubXbA^_Cylb3rR2d>C+3@fc4=l2dUeK z^Ydj-vcVH3=xrWrEo+dS@%GWkATKwIH;C_dIV{(?o?r@T80ngQ`nkWDn^pPi#uAKE z=$Q=Ni*ph|*@?b;BV>7>jg#kKs7v>_PV68v*phl|501gGW**}D$6;8s$NbGETYUuq z`OG!&urQTT<}ELET+;(h`EFceQzT6aJ1MiMxz?5?04r~WT8uPkm8NL0x6_3f1x<0M zvEcYnoGeN(SO;#2S&?a%fm#{IO9RXvb#kAS|NphemQ#JZ5%7t(n!3s$tbmD*U04_P zvI_2@8S)VlA^;+s3BR#DmJ2H@RFV+GttPqDi8IOKm^$s=M5}lME$$egN|)IGT4aak z9di5s6eThOWIIp z)a3VkTF5%U=Kifgvi1fz$M0QsCFYP-Cn2Uk1`N83rwOLzMy{4e60( zfAzF$oHs6`a6o$NaG}8{J?-cUKk7p7`dgw!!ijy}AZh-(iTk|;#W?V zdnhF<_hK1~^U;7dT2-jIK`3C~BC!2PqX$r6fY zj{))P!?CA=qq~3NLBOBx&SRe(!Z4NGr-?Jup635?@329&ym}pcPMhX zDnw2|wzX(@%@7mMg$@0y3wB-fJ0+=Fx6_zi-6KLmCbqqSYG+1W7E(#%+4}vERs}Xa zIWx{f=R@RimFG&bX4=HU^#+@dQVc6$M*uHC(7!?JJxuOrp@fZ?@`BCy)&8Nq2O2?m zz?Op0NU~y&D843LDc%m!s-})OUDgOg6~DC48Cn@u>fdw%Gr52KBY?I!jXW$ldwReS z8Oa_--ZXyCe`zt<`k~4z1gBsS*VUKDSnumTI70Pe=wZQI0y1i(Pjo8-z*ZGMaykjq zf;3&vP{0wzcv4;;YdfL+sB`NV1c(=orZK*xVujnDo~1jI|G%ctzl&TYUkT%2r#krk z1Y^9DV@QJbN*=bNUE89xar$6+A7kbrJ7?wtj!t(H7hY7>`%2Nz5A7jgnWZgzI#;5} zzfPg`et)w;M^H?V+(25hzSQ$lQJq60sflrqeWUOwXX#Bse5bs$sM9JkkDvr{>MOxn z;&VK}DOQey@+(i2e_hJ|G#|;7XPY{?35~>2$%7JSFiA_Ul}jO&6*~s0!xEg@j;a|3 z_uKE!UNH}9JHwZRin36LKKAzxyVy-Y%CggxW zBn=tinHa1y@Lx*cXPHNVFe2I=G4FZDfFr`XObDapw;t;0>R@?=R`{DF5C)Y{Yh8?m zVp73#qb6)3pM}KNZCh7LY?@l}4r{ePvd8KrZfCmQM)K8F4^}q_pn<&|s|YC$owLod78Q?!sKJG}HG<8p!T=bjagZo5QYbTH!IRG=%@@sjZ%bd2NqSP9aLL;n# zf=iOmUfP)kcg&h6N!JiV2LPIEALwak~1_jT8ELSVEE_42i^Jh67`Ak3`tF^Qt&GJx)y~+vCqG9I&7OT|=-@ zjjNT~w%1*?*Jg`&^<;CuikD07W$!?}6(L}6lf)A@qo=#MzPD$N_dPWKUnwoanBrlW zWks$&&Q|;Helz?xBq&6mI^3SxG>lEEIY)Pf%zNy781-{aj^lfZ6~k#n&L%>Ka57{E z_i1icR=Nmw7>;X`#X=?^gkz>$R%+s#GBu+pJbqyh=--Ufls^(#TJWZ8?kgofY~ zTw6L24#SE&5yRKQ(h8!$<(@WsVfuP0!27ACp$=x5rvrlF9~uVTT};-0%i4=ljY2@c zi9TYq8(=;OYYOX&^D@!Ne~>g+uaMMPw5e+ncOP?|m_hnjVD zaSPpjbQ^ju&aTW3^9f)oa`P^o)D4w}<4e9KL{vqW`aEfdfGr^FI1NA(2Ok5Dd*#*)RrXpGN%99pe3k@9PeYS$LjD zK*Z(vs%$^aDS7XW`zLAas5ocG};e`QfMWONkx#UdZA5@}s90?L`jF z^KI>b{zegl@{VOgU4T<=tNZ;t##t(0t)cRdX)+ko#!k6Q?LisuZhks=B9y8Dc5q}| zM8U0pQ~DYt)byMPw6R$HqaGTbxQ1%zZp+o(8Ys>gL?* zNuilTxjcfo;)i?S_PYN1aFVbyt~=VY>F3qZqO530rC1?i^@(XjYw{AIp^TCh_Q%Yg z8=pfLtBABadva14QN`xHf`t5a$Z(T@OeMA|iK`!2amU&4crycL+D)Xik6Hws%6FX0Ls5{)hqEPeTPm4o02)Ok5%ug&mU&Qs>lO8rS z4I|)TCkHX6y15TIAHinh-LoV)Q36{hXaItS^ci+Yk4o#Vm8WM=Mg88(ox*6>E21aF z&+3*dGe-WH9hEfM{}pT!At3>>kpQg_Bgp!s(8{OHU}x6m`0bjW(r9Em#$}Mp?;6Z9 zC~KS}_T)`yz|f$C_r42%;22W;KnBbIXFf=Yio`ZJtY#IzBpG~N4uFfvmPZjn)CIB@ zqoysZw1p1oZD-PwKVmV&;SaXv(JFTTWitF!xERiS{od_{y~xu&0nV zmKg(FkLeSaBJ4ib!XTF#miiS-I3h@Fq9}~rZ}loguvHG$r+-5=!KtC2Wn$_|UOa9o zhFha9w>szI_Oj84bSmnp`s^M{QdhFKVN8>1?+R&`UJ_Qz-{9ZM?agECX2O95WsPBg zq&u%Ke_IJi;wjuo#g_J9)qw#cr`v%BqcvQ6K6rKuYK7Or!88!IO|ap-4;3Xteyh@g zH+B|7nG8J)c?!jr$6G1u3oI(!*sZ@;+e&sxSO&UHV!=dH`DA11N0emUGO`te=6I3k zPYJ_ZPRJa=kI$Ut6ldPKBShI!#D(ZOY`}205$x2hxgi<$kcRZ%1u+93Jx*qqD8pA@ zAZkd-u7SJ1>mHL49V}kRn1Hi}FfMB-)~2&n_=a3;3WLu5kO=oElbGnsxx*pSd$oc$ z7NrVWs$uiTM_kb+15N|OxT@6ZpeP5V>P9j$1MF3q z088v3un4 zCz3BG5k2;x6N{p+HT89^vUu?0gTFIrW-~DOst&MuujcXOQcyfKn4^({wBPhQkv2A$ z-C%Df#1%R=%A=nGMi0u0)FX-*Ry{uI(Xa3pzX^06xSVqy+)yZXlxmb<*2U}gs^lTZ zkPduh0=mX2v`U-eEJUvD{$hY{GWKmsw|T}Lo!M(KyLy}?Fg)!-M@ISLI-B`bxUR&6 zuR>02X@J(~NT|(T%HHFOUyze1mGaM>{%fWo2)6_eL!byaV8i1|zdyb7VsL8;To4qE z^GJty+(SanF0(}$BRfKaNE6R%IyZ3#YBuuqX&C(Zs%&q3vJYC6nUKR1iX!>%#?7EG zNpLf#k$nLyyG}Abuua6T^CDkH;nf>W*15h|2pB2GiAS8cqPaKn7cVlj5RNC4MM^yXV?zs(O7C$LbqdDRS=Vej}hzQnj`Ykq9Du(k1A$ zpu=zXGv9Y{&vt`pKF~Ru%FT<-$h#fFAr)pK4xKV+*}gK%6ozR~h-=N_7=GicJLxsB z${<>m%I1TTy<%jJ+*$<15De)jIOCz^dk^xblHid;8 z@kkS^#vyZ%3ze@gK7RJ>WA61YGg#(m<5UW)v2!pSA=QO^BLpm;mOJAsredd5VCV%L zCfNy4!+_WrX(DecaSzj~(uX-KyOAei^?4S$hoIzj&LDnxXiz+!7=M(~z^+KAv|ir3 zK|rNEK$NoMR;n|7af}ARZZjHEWLk`IG2A=^HxOYq+3&h)^J6pjyd)Fc^LCyoN%HtS zmWv0+qcX>ViYdjte_ole*cYq;);Q`512#tLCAU7)gW&zzn>+Y(Iyi$7?{1-qe2o9glwOZs=;eC}j&r^h`@fSh*_mwDY{ z87SQ?Ig2melR*W%aKE5~N}+Lh7G-6#?&=WYY%etm!wJQ7JaW1DfQc56v1P7iWS#X)O>6-JDp}S$pZDk{{koA~o_mgWz0>{u9GDkCAi5`MWtwwo1it{3YKTAnT21Y>9Y35nLtv0&*cn4LT+-_+A5wTk51H#am#BU9P7)rqs$_@K@!~&I$Lkwv7;+L>6T;t z8Y;MWTQ`zg55=ousU`U(-Hdv$U7nj~sEpDPA`)C@J1$O5OT%{vLXs<|Dns7e&;mY06ZOm#=(A51!opE;2YNs$k1@#@#~gi{gy znhEOLh}D-$0fBf;&fx6NNTwTUGCJV;^V6(2^7TfBorz=TqDl7AbhCpFyT1F`R9Q}8 z)>zzUJcZbWj?J$o3sjGGa*XVCJtULxxw}-dG8PXjU=xP%bB06p?b66e+2|V#+j-r< z*9gIm{WO&ET>q2MFWilo1=7DIabC!PA4!p)cW;h!EG8VMO<;($s3?|vPrtHJS#Jf1 zcJ|RUc&6}f(OtSuEf{GywwSG^g=R(f4VbbuiZfPoq<`_(=Lmi!fQg6tElG0aFx?Id z_I?-wMECN6?C^2=_NfCO_r_7hPQSmsPmal3LuJe!MDBP)XwP*x$=oNyM-LYtpW<}r zFF3OA(%p-fU~BJ3Y=WsFaZasy;o@TEc8L}ma5BTqUQmB^0g2%)1Y)WceXILh>~c@% z^fw07bEx;xlN6z|()hTz&Pwtze-mWFk#%jl^gcxB68F)nD&{^@e>Q3Sn?_&SXNj8$TL0TXf85 z2vPi&7Xi0C*YDJ!G>TCxHBdnYM(nO8`Ix}qE6zb8c>M=bU(sP<%8pl?_RWsVzKN(9 z&Q<8JIAAkL;g*Z!@`eZ8{0UnS>YLqh7X$}!ZjK>KUjoc_@;IfrLKz=ySIl;p9^>YL z&mw9=6m4!4Hn9nRNRyR?HOS;~lFEgbx=Uk^)Yi#cUQp$-Z^?YlZVOD?%L3#ghEcBScZD`sq@CR4Z<=U0>lvfn~y(n62kMbHf-D^Nof&ron%_ z1MMCfv!YnUP~NWoRv06V9W5pWebok}uA zKD&^iX#8HW)w>nOa5MO&p#eD}_dWa`3JB}qSbGaY5eZXf^yL@U8wV+ViZmPe5 zWc;i4>j>_AP=sq;cz%u2L2bERdUoc1La*CR(Lk=rn@aJKjXaawrT_B5@iV&`wiDaE z_r{|Q;8d#y2iKaa0f*0Sv<}M`^<&g|r~mf+M6A>0aA(25e_LNsC#U!QbMB`fRO9r+&=j}uzt#{;Xs@AGk|bzWG$yht9!)b>c5nt+ax`DMn5${p8t z9$6-VQLG_ttR7U&$gk>D$xE_JoF4Pw`I_H(V8%GEgTaZ0@kpGLDJX@`p>s1;K0rcG z@i#@PV50u|frgO@BrO?B>rTQX{#sSaB;C&)*LfT7sYA`3V-s7^n4~VK=aW>z65YQ#E>WJqbnck|m;+BaEC?UYlI1 zscp{f&yW^K6GImB&gk!hlmhuGVA-}8Zrz|@9xVKm%sto?r2~yG`sYq0nDm(FJzHb) z!8{CQZa@|{3e_CnJp;=I8SwW>MM+*u-FE@j{oAf^Obfic!t&w11_H`qum%3!k0`Qa zzBBRU4N;pAzX6co73mXQOd9ApjId>@GbzGf#Nj922{+nTf&6!V*;O*p6dOYl$a?0+ zwsD(y)f0@>YaOyh)v`e;*}K9Fd&HwiPa5@%|J>!=ookYf*H$QBT1q9nMW%p;!#>W= z27+4E@^%FiZsqmien zAKssmP4VSiM%?!Rddn&U?Jkb*w%bHd&K|}E5U$+0TbnEf%%vhX}!vU3x668Yn60}GVizuBA$Z2nLer^n)l%z}7sh<#j=n8Yj4`&evD4W$tD zT9!1>NY*IJ%0ziwq?ZcX#uhh@!-~O|BEsE+tn(&iC1M~)*M_JS6p$h7{~nt>l$LdZ zuBYOjL1?C}tc&Eze~KM$_udM{ZnAqoKgAi3O~D|m<*#iv>5 zQhXZ)#E`NI4!z`?3(#Nk}jIkyF@h<(5Dy( zo>5Q1+Q zFi*%%qUTn4dYxcO!iUlA5UPrQysw^@L1IaE<4R8?VzAgaRPH(=gmt%@w_V`2UEjdJ z-U%@d0l7_Qx{>$&C2@Yj_W(okv_I);yutrdQ#S5SQRIawqGVi**(wuPY@Se;FHZ@U z1JMe??-;5{e7>8Xnz#>pvD^Ol|pcB?z6>t1yceLD3P2;*(nIXYR5i~0N2GderU+<5a&#Cu(BqjW6j zwTcF=|I%IpV4K<}A5_2>OdP6mFip`DQfSx%Eg}P|^Q+A7?t_5;05IdpD+F?c0*wp$2f-F>MiSfy1dzn(x$zR%?01l|$HxjvQ9 z!F7u9RM)YJ-OmL%f?CW@uu#6aQ&A^loVJP^xr)S?A>|1u?HBtU3=NC5uSX>11)omi z2iwJAR6MI4ur^!E!Mr|D0IOYy3tDSx8#0$4Mlp!+B!8${sHoc~Ui5kJZUTkj+lHO3 zM-Ih54!h=Z6z&wRM}06e4gIfs2#E#V>llL=`(5zY5aHwt#m%q9?=(6pp1Pis9goX* zKb8}ZvPU0fHHFJDiEqIO1&}MO`(@QfV zKXqvcPoZXcXNiia@A)vC&ZG*ex!dBQ-JcC#pB=snJ>{1l%kEkkJgaVBR7P zn=&Z|o{)!8r(th}c(>JQNf+u*Wgxhj#V!0Cbfay%M1mB2XmZ}b=B&Z|89z_ez9ryT zT4LlN$@~jhZASFf*W7vVQCW+mjm3M@RkQUo8Gy8!M##JQlQl}m4eX1tMYsZl%pZqE zW2jj<5l*4LOx7^%WQX|UC_LNMhZ~9>lZy?%Z8~+I&(sND@aC?CN%+{Guz}wJNR;c) z6EQ~=PUIWxi(pnw(LplG(1y>&kqo4Zo(;G`(AVGEL*JnpAt?4Ubo z^Z$tQK->Kz|MAqnOX|~7WKr{!qTb`o$coZlS59c4IOb(ci+|&n!J+kAS!RmtXI4#a24nB~f;`gzc(ulv)IS;a|kc&wp zxx(@feY!vFb8g%JltTLl$tF@mpo10KtF{21+KTD$quM-E;fjyl{oo>jFu%GhuH=I} z)bmjr1_CD?Sc*_bv1JD5OtPz=Q`=R3BO_AdqPY0{(278oyC?nf%ms>h8H-zkqoK)X zt;~d$Z9dm*0(wY<2+U!IaK$P)%FXabX9o1mqTY)MkCi|fF> z?Xk{2$}<*TM%HyC3a=L=&WvL!@oy6jGm=xVrSY>f zTT|a4#d+1?60-41o9BupX4v$9Uk zR*0V_JI@gdh_!S2v!^*$_3NX1l^sf$Fn7X&xS!01>A(~L0}6kT%QRB?zhSSJ_GMRQ zUU@cS8U+D)AL(0s|B8#y*j>OBoVTt3Q zf;`RqwJLfUPLE$%ZgPjQxF6_l8?PzdD=fc4z8wGR1+VN61J3(!9w2E-f9DX`iXdRwZa$u;XL>=X4qOcY_N52m3CNi1(^>QyQ-cZj z?0rnBn`$u-I`g9;3?P*?3I;V``*4U4(W**U^}kb01$Rdhl*2WE6mE+ghS3ucmd&gp z+uaq#w*itDeFmbF@NvGT8p=GiV%a>REL|)ju?NTZjQ>|XoX*muQdn1ek>)!9LsjPT zl#fqMaxxX**n10JosKDG84oeRg390I*l0lYx**!0dtfSVR4i_l+{vFDN)Xc^5FZDh zXl7D}-jfY}NNHP8l&Z~2<1e_SpZR@e-}9zOG+-6hnQc_Qv?)!ouj#jiHD?N;6Xq~` zpm&BVB9tJyB)K@=0;u6M^gASz#KO-BO|+ozU8w6fj*arXC3OElA@TT7SU7Cs2;u1H zAw=%vcDOi>JCOE9yY*v%UG2YnB-Jode?}o(BH1zvYdM)m%AiTIAr)K%ZIPksXx}(XfPyt>%QWBpNv(!}DALc2zG1GPbL#BgIX)Z)4Lm-ZM;q7F>%_^Xftqd#|sJg4ud&QT&G*Ju){oXFKVQEbX#fM;=@ zn8fqf7Y~L*7WXIH;Qnu~*XkEO@_oh9z@U|#6iiGfWh&SRj_w5QYrU5KFl6y(z;&D@ z$kWcb#r#aJ=hX~JI`b`0j@X0$DQ{n(GG8~!7ud3hx?=N^PnZY3@dl`o7eLKH$jqc!bj-W#h})Z+k59`c z)K_Du&6*!?gYAb5X&MY-96?WxZfOhTLtNYor-KrdU4IN>A_I+bXXLK{C@+;2X}(2Qv|>0 z|GB+4MCzhn{yEg>44NX3INS|S!10~^>A@_%x1|E-6#P@qy3LR@Nwx7xmC9^90QE1E zC9zr=Q%n{)W+W~L0c1?aaxGh23!0<3v@_6B4 zMDC@$1++&5xucu1*(i9a&MSTAs?L^I zT>}DYHC>}8kOXDBB7&gYy6~v1{+z~VE2dA`~efLK3&n5EB+?qLloS)u(tZ}jdG6->vgH&~&)TswE2TZgBp^F2#DC)IJgGWXY*mc$x#PF z>BT+%5HgsK`r9CHS?*)f38()ddK%abkK}E3JC98FDqd;AgPZU^1==M$$%||{@avjU z3KWdL;B>)`da-NTT>_5$X7dDZjgrRywA4s}H4q`vR`*mPelYCC*YXy#9~$AWCkH+@ z0cCDmiw0GZ=X(g7!+`6(23mmuG2;cRWDOkc5v#%2Bu-|NPR|uy;Deni5tma2F9Aw} zVpDe518b7bldLs_zQn=$QG5X$K;7Zr++oas;X;)cMoq0i(|@dEeJk_7`<3i9GI(9p zcj(d;8nkzl^y=Tdh;^3*ePl+sD|XPlt@*S(vk{l_e}1V6&6eKE4bgUb;9BuIA<43V z=~M;Qufe}+o#wb$P=^mrTA>eDgCy7RGvo6a&?mfN+lj!}M-rQdmmPqt!z`8qVE0rj zAWw5b4)jak!n~7Me2NFN6e`1+isYf(s(PAY#B0X@h@bUiG&(igE9VL>kwvt+6!k7A zCh!rxE+;K^yjMcdr9G1->9x!iQQ9^lFgd6rBAk=!$p!!e0Fs@mCU-%6O@wjA--Xid z%8GQ~Iy=t)iM)c$Er{<$ovA#L_LgZZ(C^9>H2zwN4C|m6>L}3GL$57^^S4w`3kod! zNU#Cv?m(L$(AxgEJd?Y1mRRPdGJI11FmH@Wr61|`Eu+Hqyf2}_eZB4<1P1N3pxeWD z$6pDNXx%AXvwnfc6A^!v1rX#+9D1QP;NYMt)Xfd7v@Z?J9z0iKxhT&Rnh2P`4mgcM zZPEJ! znR|wCEGD4chvP4a;;rfZ?-jG-+w@c)s4#+YvwEK+iVE3&TGC8ON(F%%tuiGux?#xI zi|L3;hK*X_Hgd{`wqgnw0Q4u|5os9n{=6w_Y_HEwQFb>^c?Ge#P{E3wu3i=Tw{b5BP>|AG&q>?!|L#fBQSXcu27(z z4IbzHc8d%fi>@3pA0NTPyN@8}!visbsKS?OV4rBl8Os3I^~YG*Lc!d$pmlO0Y|~22 zXw>|=;YJy7R+s2NFQ2Vu%rn$-ktvqui&2;T?`$htDy`GYTeF(KUOJ5$k9>MWCSqwEZTlZ%x2g~F2eO}?&8s9278UEPDC;* z!NTH}i|XnCpmx}Q@lp`J9?eJ%T75aPLA?0|{f2bXn|a2HU>z^lPzzbgJ(@6R?R60B z!~iB<-?OyRrOMk8uk^}+!`QsmR0Q(@!>PQ}a{+KvW8Kq*LTK1F0M?ia_@oF3Cokqe zq%Jyiy8C`3jkmgg?p*6my|rMmk3#E2K~_O-EviV;d2`>@I3d)VRD+}^n5Lr|aZuyc zzCm6|MDJn-VJSd_tunn9^dOc5pXNV@Mi)~5XXuVh7&=uJfsWTr(L^bd8B)MW?o>k7 zZ6Cx!L%g^vxo-m!r4RwU6)KBLW$p+?CNc$VAXw46Avn-O{_hA}@c~5i42hzl_NZA^ z77de?ad8Y(o4GvrA-;}&tZ5XEn9B2ZVQLl-s(e@qnqZh<_NI2~62>c1PsOMA7M6zlUv?qn_FONxl+$ zhhjT4`7#@3MeNip4uh0g`l+&1k(TYXKysz$n^-5?vi!rj5vT{C0W z^yfN35m^gyZakr327CC1jgmm1AZEL?KMp$J9*jZzbX_Q+9--0mKCStR0O(a*|Eoz$ zTj$nV>R;13;UnrspClzr&pxmqi2k=NjFXVp{l>FM9Qq?5V^PcqC@1A;>$5%64>uKb z$lYkuGou0hoKu7u77Eg4cqPqWiSM)o#~gntEfsKh#V{l2oo&9}s-81Z`bp}$0*it) z_uYiIFvffQy+WQPPT-VA1H5o72`zw+z(S7AT^#|QfbRWfn_KA)+9J5D#jIwr0Jm;Nu~3!$4ZiGDb%<+^>W!1dxGK7eEU{1&atJB;9Y3VJ7+yoLRj z+d+m5K8DwybntgVCJYd8R@lrp?a9RY#4rt&DN;P081it8C&(A0yIw>~r>Rc%O&P-n z0-53hTAPzX0cc?7lBzf3#6> zh~$1tNMDDk7=LW1uHSc}A6sXJg(uVkZ?FxOX?T+<`!@y{*R$B6I0b7c>D>a~kfm=a zq-$W=c^0~Dl{930K|J_jv*Eyv*yfTWf@*=dysVV~2LH+9gz#C-AE8kwCiU4RHFf~jrD$W_#jaiEG`j+-$m&@-k z9)U2fi|31+3R)5^`w}NeGJV|y=;J4Ca}|F_ya8r;U9~;OoJvKRfCNUbZRj3L$zL(L zjJ+gPp^FoqC;``|o~RNg&6bkX*~e_{kd7;!sZO=cW#Wq*3AjG^T4I?zB*P>#m&T4g zT-4rwa+GPa%)Q3qZEw4o7hp$d##R^>H{~{(P?QeLx0)fql5dWkJMmJCkR=`kMGqAW zhUEhcykvq^tH^&c>|;%bsR1LHO{^?8JIG;!HyL20W6O0}zMyhg0npF$yu#dH~h97=V#(kcJhW2-~4%8I3Fc&su;#3L66G zy~&U6a0Wl=-+C~4vWjIisF=&R?Vp|c#9IE52^uREu57n$TF5v0|0{$i!QmU@G~)`| zxC>zM<;HE<^I5L{i$f_bD~pR@oc2tRUs%Ji6dgwqhxGs2t&CN^Int{%h<#Ea{Qj&_ zHs*y&!?0Td4=DYWDB!q>6Ej~du4CuXP2?bo0PJfaEQp5`oI6ldkWTXi#o(rc%pB#9 zZUoQngJ@!X&z^-5L1cq=nuE#~r~b}@i=f&Ir>880EDa}4=N{|eGOigjba+-|#OyWh z*HdkwE0B2AgVnZ~`}%u?z@QMsccXsy7(ts8FBtIDg}QFsX-Uv@*D_>?m#R-z5LV&< zxZC!bheP`p-?#??IVcTLSV=SX+xHFc!PFudU-rFnd*#3X&7xm8=XBg(F!uRYWUBOVH{J9gn$A;4gIm+u-SH3E?St=# z(m@Ix?AtqG*P(xnr01&S>~ZwgbGVQ`FS?U!D<6)Q{d%L=GO?ACh8^IMlrI4Dp^lq#diC^CIrcVL@LggM;68Y<>Gqu*^aRoP916m zyhk^Bk6XN=H*qYK`Gn>YK6?z8Ntxh7v#fs4E244K!}+uceluBcs(&t~4G3 z#7GK`DX2%?S?x|0NOdBikyh0OT2Dw5!wkVCwHBY{<;9%oYAj=@Dz}+95Mv%o`w%VU z^%95>({0w2WxPV6)%l;(SgyuA+S#VVa}wRY0xB3`^b~6Flvt8?>jS=s@#P?Xj7iPR zk-M$wm1%}B{1Mb>CfKo0t~z&j)o|*SFq*9pU=fH7ysk5=_$fT9DXzY=$N;DQf#f$k z-N{vdQ6aW7Is?Z#p(TfbUb%!iVH+xys2<)l%&cXo))m1sR_#kJxBkqTFm3-6hv=*A zlHK$2x!FYHiyuzdo%{hqg#1aHyz&s1c|LYy_8^qW!jS$JjbX~j8otSoTXNX&`R|ms z5=Q^D-+0BNZqVq|1QIa4EtKND3k6|gXS(WZSnqZX47==(rqs1MD<^-fGSnCy1++MC z3F_1uo+C_*#9OgEnyRzol>y31VfQGzBZ64C0u8V>kpq$y4_IVx7huSAYc*^McDjQh zMN6FF>y{ajFdd6B4ZWwtUM$EYp{}(eTZXp{XaMhzV;hdYeClHbeckJ9m5*QGB?`R+VGA|qZp=4Kr5>})~BMhG&?l63_S(Pub z@fi9^)xYmekxwRa$|3xn7-GengYjyo(bm8W3|xwX2_r6DcUYCCF;y&<_9nF3S?Q5A z#C2dMc-P}R98(c*g{$(Z`)r8!W~e)#)-(@VGXd8wreh|zO~x@__6PdH1bXX!#!e-u zkD)peHSNse>Uf}@PuPumwD~n;fs4vUpjCHXqf*OI`KKQ8y}nEvytowmaYT0hH`3y! z-4Gd3)FkuD_ezoPQ=`UnW8gEv_DWZc30ee60JBgDc}WPQSbr4Do%)a4*=_kmOv-Z7 zRVx`(=s|4c5T!O`aQC$UF7_P?>+O=;VY!AGPfQFy!H=8oUQO&NVu!xe`ql9K96K)5 zW(EV^hl8TVsF%xd!l103?YQxmt5OjQJ8jZJg`v`|u;F*8+q+NpIuDA#xoCKXzDFEN zW^XMBrtV@4wMH!-;bj)}|7P{7LxWhs~sp}4HdJy zsK^16O5!mH_&Fy}%KjjNg(X~H{#Xa>1iHMmIPY_|_!X~uM$`s$%Gg$_8^37V&XIPE zlL8>?{Xd7`&Jw7IQBxHMA zBgP>iPL8D5v<{Ie2XssB_vhp)`b!ok7r1--QaQQOWITxoQqYXNbb1O`@ZJ)u%%iZrGXS(L3fDiSBg)bZTA6{(VC?>@= z!5Jnj#Pm*ii}1KpZByU0yC@AN_7m_rt8NWFK?GOKLV#Z_^xTcDR=H#~~ zwFx$>F8Cm_e#u0O_Uxo{f6iWYsXuNRE&6_DaSr(w_8`j=v!w`E7-&+75`)u-=6&l_ zM&gaM7v6V}V>|u@rSGCkmgya?@*HV>+hD`S6w|TlBiL;$7mU0MRkSIX0p@fJH;F9f z6c1Q;B;Z4QZT>W{YL4t?>VMB`X(UVo!hAb| zdUV7S9b>ufwxM%T)Z+Gprpi#KA3c7^_QKH~jK|}*L&p9UM=Cq*g=ns? zu@{}7%dE;+bHDfvF6J9%ma6h@Ln{wc!}%Nd$lJfoO&4Wp)NE9!NRA!d^1{|Bo}42Q zIq-@w!o=*wnUpt^U6c)$ePHhqd0O~MRbedI_a)16@658ZGuM$pKn59{4+S^1;(C*7 z<^`XsKRuwK5z>N3o+>qtE4)jQZmvTc_#*7|>aE+i$Y^$24(a6HnV20Js|Pt7jDJXZWq#Ra=n``w0_N)r|nEnnvcJewDbsjRk_ z!x7@=x$jeMIA?>>*2kb8$Ns3Uc2-kBw#T?BGQAFxBMDNBdA3vH_+RbLMNcRpMPNzl!MkQLoe1OvTO^p~N z@FV0;>F6`a$c`PGTK>A0krI4R;AOSP`s_X(q^c4JX4PsuM65mRyBrXRn?inaoKkB~ zUH<(%$TgFBDimfTM(yyL()(xOh`Q6LrkeN1Sa4O*!kk@0=S;6-3FB(Vt#dQ$X_J&J zdm)I2AFVturvmt%y;BK}Mirv}Ge|@q?^e0JUyh=!l{ylG+~#+8OgKMs9EiY$!2Gp) zp0UKN%<9bBZrLZ>`e>|7RR_ouGIqhO*C-iOP&1Hj~F z?UVl@=LjyR!+gHW4O4@%SqHn{Kjy7U-UZE`kixBhWic6x#cvOSpEgQ^@yMZkH@?i1 zw3Ks>#_RU|hR#vTAi3iC-0s`AG-^rva9~>sB>wn*I0v_Zq(p%B!+}>M9n}l$JDq9_ z-u6{g!16v>d#ihYYFB;@^pF&+8BtLFI$5aU2WUhwNcq|e{r3rV41Ja@}GKL(sALGlkpCZuw@#C zV{d~0@wsb17fNw*twL8)eosdWGJ^qSfyQ(INkF#01y}##-FBuk_}sPH>SQ}T&bBu| zC?0qE0*!*HA*)(2x6#Yu)EAfnE-Ej6lU2i17J;+PkN3TcYfBC`{dZ2zTj!kN1B({g z4LXIL<$jk0>uDQVo3YCx^7%2Aj&)p6TrM>plhDS{feZjiK(@bBbljgCxEucxvo>?I za==0BD$YXqKe?zUE#O~1ToZZT56#uA%s39*EV7($n5~_~W|uVA=c{9Y++Zt0ME@VP zkX1s*p+<_BPp1IN>r(G>?UFTE`!1w0sutj)U<}Q0LEj6V|GCG%m?q3JIN39Cyc4g^ zFg(Q@<;&juFI6hQ1VaD4hXz(or-SPlbLEnWw#fdXp!>eisEHX-N*m`TnpCrmiL0M5HZo7P}+<>Lw zqio!*XqVCkY5kCbEBX!=R8G5)61zRe z=UQ%-D(=aeCp(eZ4d1yRPyXUVPb8bX%s?uB`=WYyT~QwUZy|5cks%=}RF|?oJR=Rw z@`Iq7`?*SMC8P?|W9@56^p)VB--4x5r#tzTp_og8Tg9T#a;TrdvqYM%8*xJjOlL^M zUn?U#qmv<`ZkG$#C#S$Iv=%iXrT~_~yhvoZ190u{F(LlPcHtghAo* ze}xKJ?OrT!EcgXdsmFJIXq7e=-qhUU#CMTd_W3q^wyT8ny5n1OA$Jj3gen~|E~T|O zv6{TEwJw6(-Z6F=g-4HgB?4M~Bhjo@&M_Yl*-5vGXtN5!s^+S@;G*Ptj8FxOzOQ^-s3J2BY@c}eU z;Z{JDWq9{lBcni==i}l$yjWoKsvwY622J}kWF>Upn63A+yosE9pk)o4$n5cb752qi zgSjl}Ws|doHSmNYrls2w^l{xhC8nMA4112x#YTcCXY9pIV3-+v4?DDE50`(BGtS z)o+GBExEM)fy8UA(0TbLQLVM+K7j}w_eJma^#Xnndy!3Bsg4pd38lkB;ksQ-?t$@K zM~vP3-U%U6xk9Xq-k%)TduHO-oCp^)uTc^P*Q2>-RGs7%Tmy2q=U$%#4HYpq?E$-f zFPA2I!4F`5zPWv1MfGR9vZF} z74Ddz8qjDwW~Wo$Kbz>X_rqJ2HBYH!@#$lg6(|SK+X|+z(TqRqU92opHjCQ2k+$o$ zAL?|-{KI79++;N#QW?#r)O_X?V7m7wZw`DleFYh`RzhHw~de6Sy2;mMi={9Dp1X;L4Odx74n!wwG$+b;lXBq1;f;vX$d|UZdRNwZ<$hUAa(eKLPSB9Xo z_|+S}e!xA_!oI1>6h!F4?x3C>Ayq|HfO8!PjF%z{>z5fiHCZ-Y!~_y5Yz6J-Q(chd5l%{lmLF zr2PQ|eU=bEutm|CB8hmRZS9zqR1geY*Y#5SX9W`fW`YYVZ$Q3o4tjTTbbBG;GZ&Kt z>UoQ(abF+AYWa4g99&!{kB}#bTYX|>m3_zT{Y9!eO0Pp3e5*+S1I!<4ots^O$>s$w zX3tfrF_;J)d%5U91eJeH`x#aofo%qc;v8^1P>we4o$msr@;!Snn7*OMS$>4wh>jf# z0WJr2GK-1VUzxDD&uDP@@zjR^B84q;bi_;x=2tt>9hrts-DVTstv<+XPkl@ra7bRM zq8Bjs*9|t*+wWxp7HVAs1G)oMgeO~)BDpZ?o7L|^T~*ds_iK)xowwB1J$leLH9i*8 zT73g$=KxL1$4@%y)X3)lQz5M;?U6%FwV#`#Ikd%H2Kj zY~=`K3|p5Cz>BHK8a!-Qff60B|6CMz*K#Dk{2w?0Cb-(aF-VzDDPD^mZ3s2dyW9`L zSn?TUJlh--nb8dIW8fE}#4q(8G230ooaOaZ-klo8tv=TfiGH?D1i!;qGPRc~e8q#Q zA%h0;mvQ?NU)BPH3ASREPgG&lehzUn)5=%r*JQNDxiN>cJtQ(=)nRIyHm`5z&fHth z(sVh9O7;rtxkNEt3z#L&Px0~7vCSBf^#b&`IL%#npjc+!R}2-z4MW$l6M2EX9OWkQ z4*t#6K{{1VtgTT<5tvyG-8qxph8)|_bPhuTc z7`=I8*EWN;I|S#wak2-nD^+TY9!-?&Y%<(wW878*b5dXXxxZw8x@FiSa)dD7m;Rxo zg-OV7a)%U5grS%>9>p|WNtMFLp>Mi>t40`9@(dR!g_}JrbM-tb?~No-QvXakP~4QC zu^7cz`Q5PLK3_uQrf)n6%ObPY*N%>OmUIqDblVM)Wu0&|Iinzt4MDc9?1(1-&J!$- z4%$f*)ZCsPGq=N4{+ZSIU-^hzJu8RC3WPK#E%vd&RT@4r_dp%|d|QEO*&&2-vgf*tER<<&XB!pl1kYktDUp4Q@F zAv9I&TjRq(tm}_huultY_(pXQNZ{wX{X{>qE=B(ONp?dWtwxjkin|rBYsPVM6=n~} z-y~D>h|C8!I&XPfmXO5K0wscljDl|@UJE_CndQI}*2$j|D{tfE)aO6k>PIxGZBMP|q`aqStG$qpgN9O+cb@ztNi=BV75) zgHR6Jx@Juj%4-vfvk#gqn1jPgh!n|`UB3jLXvB?6}L+f z{i@92(wh?xk`k`m4vo)zz-yI7fPQ0%0hqsawwF&UIm2BbA9$x)GKaExVcRVHGDez) zao&x=c%69TAt@E1)ioMT3uo$$9F)37od|M&#laBkmVTtT5LYxSsb7=L0W9IyP2H-YOdUBFupKWJ^3MqWT$491-nUC{{6Q(Wo>Ctd{)Yz)U z-VlqmUxco@pJbOIBsn!DS~7{T;zlVMc;Vg>R{emz5fhJY}$7V5OoD%I94 z?pqInzUR}t?f>Xorl0Y5ulz>9TPtI@#C68wudadKp5BtERN}Fi*)7n9b0X}M zB79+#NpnC(5bf#gd)zl+Oue;Vy~DcPg>52#sTP!i01#*U$ur70AG*fU$9*(ETqhoT4dU`U8J^P1#Ex(&G8soN?BX}X=#1D8WR;vlDV zfq;dfRY|MAP8PjqfbsDXUwQQA9*BT|?8a)`^0&>neXPMZF6JE?koBP^d3s;;t94hL zwEf)*Lv~66sHBB`!weQS#Y0^@VMEL5x8G1`E zJe@paBh?d=xSDK^$6`+*UI?p4##(6KnGJ%H7;c;I+cekyv4YybV{m?;ySesCRO~bT z?30Pq(X2ge`Pgm%T1~>;PTB+R`78tj`N@v0H_o(!-BUoeb^z=r_kJRMfT9B^Zk6|E z1G1RHr7lf$x=LFq*|;{LrJwZ58f1YqIw`LS0uvc|E|Oo&IbC{^w9WQfc9#g2*qi zBLJm(@!0EXWW(_$Pjgmsn$W>{OSd*W4(ah~*#2@?4Fd+Sr3L7V3E)>}s zTfq7NwekyY3+w5Gn?;TI8-Ozq?iO#&KZt=jmVt%b$`%bXLHdbxlQ?o|tO zb)nUCC<#Y=n?JWoL7kBv9CaUdv>j1nF%;2RnDu=2zTeV>v-hFqi|tvZTxiy`#-VQ% z@*M4mRLwh@(7jtC8lVs>tkfK^8WW?7)z2h8v}kF9@}ZhN&6e#7ezGWB_Uh<`B`H>Q zd3!+^-9MeW2TEMxP6V>jg}eKPj0J@BefWE(bLWBX;a54XQi2xJbFLF7!v@w0Y7I7@ zH2g<46Xh7zjHOxbX_Oaovm%eCDIx=6*mf~^5zY`0RIJJrjJMwM6(3((8@(_5{GBN* zcD|X3hsN0#nWAPkx4EpsZ8hl4@BkhsuI^d5r#4sxU;QD6{t^#!0fpfkRpaYXMf*!H zV2-dtUmb4hv7L6<6b;J6%F+le@uyh+HaYSp;hv`CPF{Cae%vUXo!d(Mp8134sO&GM z!AuIZc6o@6b9qS}<%Hil66HDhdNSP@77pziFQOLT_Ii(V(6-W^^KzTH7md$daNf&X z14zu`#pUw}G?TOJ|v3HJVVnY8Pcyu3_E?kRgScgikq(qC) z5Sp&j2~^npT@{nwiHS0o7kz8BNZFvC0FlBqgBUH%sCI31Pd6#fqt4lZSRJnI-T!iH z#uFjQo-V=pbm*Kz%=BZ$=57M4V8A`lXGV<9?NZD4PL42XEr1wkYMMbIRKe<$9K8i? zO?ufTK%6}TQ(uXa1Tl3GfFD>MX-vJ7I0E~E8^y+?EnbY^0>h>;V(C8CCGvTl>D;LP zQMFPU0K_@c)cz7OeuD!*ksSujh|NCHqcEodyF_4=3i7>?^+}A4(`^iFhX9lBEaDYV zuh$H6B`O;t2-e~+hj!q``fSh$ljpZf=(JOLs&_?QbLWThaYlG?OH_yDju>lUT|(k} z7s(=KbGXDS6TU8Vu%(P8bgAGeBDpQlYefboS%F=$^^xbIR1w@nQ&3=EbwDutcBn@# z1|2Y0Iua&OP1Rs$?zVKWCK27@VVOuf(<;vS1J&p-b^jW)BM?OePzvZ#Zq|J6^s+6f z4h3p_v~z%hJP*KsrL#d1=y+6;A*b)84bj)jeDw0g#H_|9yuat8Y`A3L*@49-{DAA6 zeu*}O9+C+SH^qt`w>t;{XY}Hgspv=#9bx*e=7R)>DOMbXUc+fmKDMvLFXU2I!#ilL z*|ur0VO%L2u0Z@WO+#{N=bs}^BcSpxPW60wZk_UD3)pD{`6oE_T0#A85Q^(CwHyPw z0HT{$w{Hh<)yxSS<16z-#~)$z+IoX8_cwchzU_SpLb6D9f=#7iB&zS^2(`aF`n-!O zHj$*J=o;kUb`kDj6e9BUOy`MBCu}tpnmWJHVa1MeYv2DKN`uHDC$~sQFtO2h0w7;h z#z}tBrtSx?d)feqs`~)qA}AdqP&Ci|bo(OfXl6{?_IxL3NmMLF5A&2C*1d((!=7 z>pK&d(&@2y)nS^5y4vWzPWIQH?zTH(IgPcqs>0g$?X(1%t!Z6{lPvZbA$h&zkE*FD zK;bJSvH^lX)a5z|505S@;3xk&xu*Idqw47B2@qi2B%fyY3-ODksQXCU3)(M?g}s>=GMh-KYp;k9K2vz4kU}37`5S_i^vi`LUcSo_^{1d9?G9T!!3O$ z1VarDAx|bnLLq$Rw8S}~FfW3wSYZgqy8WG5>gw^NQ1k7DX?`lulgg5`A&tx;%GK^o zjzOqq-MjG-@q(%Pdnh)5Sb~^b_Y(HtB!JG`_-M})j}Y>nPOflh8G9`i+;UD)sXzs6 znS=X2_Aq3Q#a=+J{Uqk_G3x{eb{wWHG+=p5LrcfGA5V<3${Z)$+1Gv3PoDk-jP)A4 zVzwtRM5M>67wb2u+SGLmTot54BjuT{$sPBDj$N7*(LSiD6|#ny8D`so;mt8h{pmO+ z?W`%r4rB`$a0y$}-PNIUxX5pcQ!ZqIDd53zmNV&uL0(QFVBbrdcnA{$!Bb^9PI0`S z{H;~ZOB}3Oj!CDNEAJmpLHn{WHb|F!XXocvZN>YBdLXrG^#`3w;B+r^Dtk`20iKmu zO?~&YA+!WFKCF4tO*f!?jYwpS7+E8a3D-vH$%la#I6C>Ik}j$9UnS8ioMyEM+ZJ3z zI^*$+$v-9}qw@?!JSOz_x%ZA1tZo1n!LBRKbodgoCMz}Agg!q=;AiHei-CzQ;xuSI zh_6S#-a;+evmbM5i$uTe)1*KL7_O>8Fl~Z$50AW_IRAG<$YAoGLM#sz7yJ7u<#7^- z)^^tsbYS#ozT7wJQ=D(~6hooo5X8zaL|s(cJJuHkEql|+4Q};w{t2hwhgDvq^*Nde z2>pq@6AWG^Zirrztyl2VAgJ!MYo;61LU=9(Z!{&(oLkNrD|XBM>WI6hki%Eh_)YU0 z3^Ll3lvR-l`dD_-?>;3sq1L>qF$PGnI6=fwr?etC>?Kuxa?hqoSdWqT;l3j<`&%A_ zqs0dh)1Miw@)!*ZVuteUHmK7;_iW7eNcLxG=KN7xy+3Mbc%Vk4q z0rl3Csikvwu$I;{JU`tVD^6%Xe2rN#k5o(pjLjB zNeMpZ&MwFaj>PIUw7~RAS`45j@&D%fVJ%6xTc=#via;lihB94h;qMx?2cv!Li&*Jb z0S;U^mSon&7ioYl6iH>-+CH#smIR0$j+_F~!-oF1ae1}$cS^%iM7Z3eyE*lLLdSOt zt{0@Q(80|IJkALrSga1BLqFgL&VtQZo-tZ8G{%nLF|O^?83YAJ%G>U@w!r&pUY#iL z#oH+cX5i$5$C&>`lm6E4RnGt_bpnV;Cp2LES(>uCg#*jr=Xim7))s`1^uLI86{9J? zuWuXnxf^SXzE|Ld4M=$PKNXTlvU(qHG8e>Ho5A!SPccO+Zj-4s+qI;^{I9|qQwo3I z>RSG)cyE`5g8JtEufyuc*DFD_Q*}6!Vkj)c*v#p_YH@;LS))nL_YyPPn%cX@bBM2{ z#f1@8QT$j3CRz{bkv1%|T&=4-iZh2Qj4H_uyXgCEu4l9!$Fe^^q%{s7eaEjp6ES}pXjSZLITWUNL$Y1>UVlc_*NJ1 z?jeiw-WGIO^zp~p;h8#V*~rAvB>>bMI&&$GOP7h{y9!RL>h7pgfK(54fRN{LLB(_P z2U_tGqbQ}_L!YUf=QdqJh43kA_6qQ}_Qq#sz+Cete)ZTy*ekhv&aFoExo8u|E040G z7N~!(3oHG~q>IV0mk>C%xln2b+3j@cJ<6SdCG*$Po8)qD zLH?yKxZ5*4kGXI9)@pby!Szh4a)fgW`tO{ssD&a&G!%oM)z1;Y8eztRz0AoB`=`TT z$I5z@RmdZ4!HAfOI#U9WZ!O@<&R?K44T%CaBVVXX8O(ujasfNgPORqRC6XO=07T-} z*U3Oaz_HXF^jp^ibYWGC2}hc@xxbon*HU~Pgu8U=t~*HMs=%B;?)>ng&MC)!xbw%T zfM$f205=D%Lg(vDM6Eb+ezyu?HNl0)-r62bl=D)L1exY%>W)aIzz*hk2}?Ylt87cf zeV;#e-Yl}Marle3h~7hxT0t~(#PV*SG*m_r+kR<@Z-cKkl%%ZAy3z@dW{ZUxou0cW z0x^YLl-WMu9cuGTB5@Gw6)H~MZw%%ID(hbd#vZT3EYN9-(uf(SoU0lqhrXM-n+y54$hcl(tJ_t&Bh;1~XW)ic2PP zGx}}3*CyDfo*KF>q*gDv=BDzS843=0dMi-MAW;u0?+w;=7UeG_m|kdp*>mwo9=-w< zjn_DfC@131w%pzQ^7Yvk1M=h<%R6`aD*bw-ggH641cLFFz_ET5MP06p4s{fTl}c7tDg*L+#0WmC-B4E11AU2N9d2pk;m252^Y~q3_ z@*uU|*+-?sqrnm#ZnI9nuE^eCRTr8LK1|^z@*SC{zRGePYedo>i=Ke7rwsQ7hI|+p zuAM}e`6FkS2_cX%x$ollyR41ta9xEv=)eJK&it8EXEF}pilZ|9ptJS`oVppmOl2fX z6_J>qV;Iu&E0^)^lpXQJVp3WJMv^+RNbB4G-1-+G#sp&57+)4HOp&(w!DcI!0i+}B z$J4r{vI3zA{|2~hE$<94gX6?hR1xm|Ci{15O_!*@#BU$ZzulXyLd%zGCFQ7!sKsny z>x4Kz-f4%jevBtIjo4RKP=F3+9&tcy{#0fUkr;Th8bQ4Fkb_b!GT*%E!-YKf3oxWZ zK!KxkX2O*Jb^deZcz#V7xTTnN$tDyqPepyx>t_G)6$?#e<1&O|t#Al{9~V4SLGUT+ za47+WsGa)sEHVWsc$2j`w(2uwv%cx9j_H%0J3`C^w%jk$EZ60Zf|@AnSn$tR78j)H zNXu^}7O2tA0N%#IX{PMEI>$KUqQa>vBXuaz^r1PS{4?V+iUZ!M3I6pX7wZ?VE1^Z{ zG~To_57B6+x>vp+?im4jnP**w&+(~TL8ty5J45JJy&$oKQc>*N@;#&oAvAC*Z(e9|2?oi>ZF+XMPykI$TeO#@x@@{6c z#Htk=%C?f~Io(^@6!Pz9;p?usHfut`*XgC9mRO#apOY6y(O>Jmah1PXIfD6**B$g< z>I6i2W`)5^Z-}ZAE;D|ZMnQI^p?H3IH8^bJ?gZ#{0eZS=R?FfO;Twn;DzIna<5#PL zgeY|bjkUoqJsGuJ?zknom{Xbhq=TlyZ(gnxj(w$Etm5*5WUa)3e(7GHVOo)pvv}WB z7_M@HO6J?Z+OQXG?*5vPUnU%rb&V!EfvD-~N z5x?}%pUi3qFHVNeSEj7r^j7{Nz!+u0zv&0cYXH-4~xWXY)<8jdd zdqXC0xg*Y_w0Km=o~|0>`g$ynP{-d2Eo1*=ZUQ8br0L>Vp}}8VgoET6EkF<7uC2l^ zOhNz0=-XvK7Kl*x|DK%c1>G>L8E<5T8RD41Yh&Qj;3>mWXF&j$ zF`u}Um4|%c?mzsv_AQnc&_PG!&cK5*CkV&FHi^m66+?O}co}AIm}6J5J9c|(9ZDf? z+*k|_`91(V!5V0J7VGA0jsdC0;9kFtL8Wj+8p~ydxa~sg=1eSrXcxjS?v`nwdg$7d8kW0aL<7NRfECV8Kb; zB$8(@&OB$iBx@g7=4C?w?)Vj`Bz$9_;{v!rO+X?AydxRbxc^CTig*TM;L|bXDT$Sq z^aFFm_Dr=8+H`;&Xcj%-^UkB}6hP|HQ-QYsG*K?p)6Hk1PONMH3`xCFKD_v&#Y;)H zfr3L=VebAao+3()n7rNnJ?nVQGarb51f6P0tVf@aPhi*p#;87do6kff#9|`T(C1iM zt!}yD)tW$Z8dQ_iKkKMGi5LHAKCb-K*&!(2Q-#!dy$D3b+4x6{<^8rKM%gd*&OI%! zyCG1=@zX(`fB+v$XVGBUZ%J=AawbKME%_Pp<^iD2siu4Q)`t=WYLvW%A((M**--1O zDB$A<0CEQ3rnNuumWcaZP-pLW;^wvy`*!;43dA@Dem~n8PHu zLTu7mkp|1ow{$Uxc)~x-*+Zt}btC|86WZj;A*hXr{=IZS!%T}!K}$H9Tw%ck;iWE+ zcPXfAs-;*xDr1%?BGIfgOK@pMgX~I`BnwIU;49rWjaH*=#g~}aaAfA{=K5cbmM`h; z0W>3Gkp}OoOXJI*=iB0ub;CbGTJb39uSc^_;m2Jl(Ehp0I{yW z5!#w_I~VfvD)v5B2!=fY7}5xg%Yg~mkS>G|wm#JnTbGWxsi4^n(?2MGVT~LXyPTg+ z?}=+paDVT3D6F(K_Oweb&Fi*+@awJ<%jQZ;Tr{2s_r3dK#m;(*r%dHPtqn35pmLqw z@Ve=-ZxRd{bXQcJ4mi5odw%GX%H9@{mgkiLRRW*I2mINzO8tUUB9G`>s*Yb6re5A~Gh8+(90POrI1{)B*^g zq0-RBK+GAIN?n@+9*Bm}wgoa+rpGIqUEFalx-tYm%Al@c^CD&5n2(mESSbQqRlKzH zH&cs-OHCNTv{^X5HBw3uApilMn|fFT=4A&{D(wFG?zE+^ToWs@jaEXW#jU02%k@h= zP$)pUnj6kWNCW0>Gmx<;$Lha>W zZAT(un|R}1Pr9*_li*o_aei&!uOZYnQgyCO9&6QCu!KfrdDf`3>b8^$hhWU{9ywb^ zn$kg&Tz}Jc2pW{RXTN67scJdUml(;nin{i&AgN^qmChcuVaOwy|CZZVJK7hDyj;c?z`B|}Gza{I*Ax8r*CB*& zu&yvpVNwAU6>w9D@u+&J@`EC>W3bX9$=xY-u@sB}kF=*vy4@TIfx-S*wf!GqqEh({ zO?RU#Uls8*xUje(mfni8k&_UNam(@G3;Nj3$k@Dx$nKc}ntP{gc5JF2G`ujWxD}jC zWlxJSzi#GOPgsuY>Zr}mhd;vv(8L+mr`}_59s1Higb6{mhW(rOBc`M=0J6a~8>|X+ zNZKxN&x=g)zr@5XR(rB;e=>1S5Xy_a1BevOI9Lxy?2X*fcV5V|U92mF9Aps!x!gIf zHAF%7vblGLh7ixfkPen>{8IT0BA(_Zs#%1_IS@L+VbcPa{D1{TofZ-R;Z{=$qqSvf zwKbHjiH6||#X>o{_yX6xM=)`jFH_X2x-vE)eVQ{g0tCLI2Vq8R4Q&FoQ~rh4Y6q*0 zqqW7;?7(05(t0Zw;EEpNVK336q|OcO8l&9z=E`Ku{sJO0x_6|$&VQm8!P;NZ(gA}n z6)QE+ZKYnD3*g1f5IYvau}Vcq#CgzM{>{Y0;v9#mt45L}KfSsUhhylbsQ zpgy`JJTZ^Abg(sv?Iw?c;xx^;(II4z6HHdZiUmk$RifRa6RhX}6}F<~&C10fpD=D) z6)9xs`8;g!2Yk zTb5H@zt7R_NOm$e#&0Jd>d*eSzYL^u9#%BVo%tQ|3>2(w)wR#65Ot<6#r)TC_(zcQ zBq@KHA_7EN5Xo8N0sWah{fkV?YNMXt@&O{;zbx^@*=&Xq_h?+xF3R*Jugr+D+w$<% zn(;Fv2jPO)1GqS%HU&OT&~vMSK6q4idV@XXqAGYe3k1#Xfoxqq4EnzmUf%fKz&pF| zw8)7O}fu_3Te1~Qcl5FZU=GBFylrV#;0cqWNvbZ9{&hnXNb%Yjcro@UX(497kj8v>R@%wa+8?}*ExF@CBv%T-t z#~%d<1zYaPylm?qqBt$l@jh`w4U*{e&sT}gXNMk!5$mHMZ)L*sWANsQc61y9=7dMM zZ4Z-b@W|gf29N^rM5l$XZGmK=i^}xFEi7}9m}>gQzEm$*djO{eYFjxH1}0<|*|2yF zm~5W~oZb z+HzX;c_Z1W_8>W&^5{^bV5e94=$uHjPI*xX8j}|ht0t~No|~1m5~rfx#+VV*LfI); z+ZdZ-0qOu=yFAs1dzuevwXFYD>)?*?GgP15jY0Xpm)jf&K@2>((Cb;V?nLMTWk%An z3dfg#Sswt;blzYF4u_Y!p2Mh*ML^{H&;B!Lmahi88!rEScx({g^Mnzu=9S89A>LDIdj$;OX_h8TC*G1Xi&ByAVwrliJ! zgge6SjG8*+_y6PD+cy@OYphXxT_e*gPT&bx!@}dORS)O_m!*4)#-x~`7@>=|Isde4 zGP>n)%o&XPA53GUET65hxkv6??0y7R7^Uw1pZGu#4?>NIys7l4K!Og+=^hq20qHM~ z!5;%Frl#w3s;guw8sG{LZsq6xWIC}qalw<8L zuL-JIR`g;SzAy0@F051^Chg?u>ETJHwKX)kg>uB@MINI)Qbv%bBZsM3z6qCWuh`0w zd750aOv6Y@g~;w_wq76}T(sPnv)DxW-K7_AufXsy#-U{Deo?+T6>G=lWm#^|Qoj4t z-yd##6!{y~mGgX0DkNGqg(P_BS>W*28?ZTLV~3}o!QBy+^T%x$^5xAHYcK{tg37nD zNP!hSKpqS6;TgCjz6!Fw>vjci{P4@??V7JymU|+{OpouMY9u|1C@@vkpf^y=9{GjT+1M@6oMy%MdjNr z13s#;>9U18*6W&sls=F6IGXMO8km9zW7w(%a0(NX>Hhl^<5yVT4pHB+$ zT13DcmW%Rjrg;Ug@FFQ<*_+WXw+&$zq zzw4a6AIGhg_m#*!jj1PRK=H5DbSWYTsaf6N+Lc{;9xpAEcT+D)KmOuNxpKa|rb#lo zQDjnZ63>XJ(?Hk0g}R%GFTg;w`!bcqmtM`<6VrLA&f|ERng4EE!@x=+Kc5#U7E2fs z5?{9@$C#)Mv|6bW3|{T^?vjC&J9QG}C(XtRt$kE*AnvMmF7_hjG3^GUmn%s%C7TZ` z(Nj!GSFV20tA03(5vvyV|6%Co>i&I0$33IvyYw^8(#tjhid2|a${Q|ymL`n%* z$af%XZ?@mncwcjQ`Z1%ZwQ*E?etmv150{I#%CI}^10TX4Ia^5ZNWRFY1ZY_g{a*kYZj7S086bNJ zK(YJc%o>(#kaS0G;gi|4ZB&{E-iXYj!vTIvRTsAgT+hA^?hAG7+iN`o5al>}V&lh( z5|nkEQ2t4m0os00+{O=Npt@tYq@csp%gTzzyRpvg1$pVqF5nU!N&Nz{3Ci{LqLT!s z!1Yye>gt$$*B{Uu<3qHStXi{{xx<|bStgqVcJtgKDLo|Nika825wTxFNKQ)(1M?q; z2V(nhb!6oPoeL0uHndAAr*5H&j3F(YE8qrB6Kv27BbTD*+!Ro-&o%Ew zKW90$vykgq5_M1(PH9!cJbh4RpsSD4PC*BTWwYF5UY!{NjUS?5a~8R+=wKr@OIZzU zrsgXT5s@$sI8?2ex2JB8n;b>oVFzuc&Wi6EhZ!_c^@Fzb$aE!CO!^`zH#PVVc1*fz z$<(+}$f&d2oDb$3Q1XWISt1%(HqnK@;aeiRxsoB{`rPOZ#SwFV+<>heNz6b!h5=F+ zs3_gq8jo=dtJ^#rP6)j{thZ)AfGOxaB(~V24-KG*qoIemv4o6dm&aYT0xKJ7iM;*# z*o(NM#u}fhq;E9~Mb40-lUp}_qafcCY%KBxom#JrDjLBaZ4sw>xv@_p#AcFd%NHdX z3H=)O@PSv9Xk9_^@0l722-!~vyi4%QFur*WY#AHoD*x6TBmx3%F%uFZ6h@!qgSlA|InGM#&&DGfQ^Urlryw%UkLT*9w`=KD@*M6DT^&5hn5f6~wdN^7 z2Og2mQyc?quhEAhTFWwTy}!JxZg zjct)1)kxW%B*HFZa&y=+xpr|E#oJc6B$E&EZ#^bpPh7+Um*$afBR)=_i@+f; z^1?+UufVy$XJv7y2YrKbPD8hp8vFMBIafB+I(Q%WGQB2Z(AO0u*5LO^mwoAq4sid3 zJWPJ4*a0xLd!1Isb9+ftAzgF3Wul&f_2W(eVBz+Ly-8bAHgsBGNbsFq$zSPVO=UE; zRUyV+v!AWzu|bj*&P6vazcRWlZqry~G z9rK7F0F|#l;1-$?0e=5**hn0D##38OdFH37y&-AS&0cbR4lAWxWv2!uA4Ajcwo$^+ z9tmJkpki#g4lT!6)}7pDWsMA63{HIn5VtzVn_&^WXOf{PITi86zy^87&!loBbDb5p zt`_a|+=JSgaW{1)Kd-RA6OowFJiB*A3>{K@1RU|ex@WX$vAlP&&)Yl}?gMqY{Je&K zlu6-*V3zzpN7FAhBC5{=a=y@*wqrBBk$jlNkG8Ve%{lqVa;tHn0YcdLR?1Xp(U?^t zX|($BIIRc?*~^_V0+=3~=bB1!GZ!co4K({(yzJ9_2a@W1Yakol7sEkULL-PDj!R|o zhSYoaz`Qn*#nJxAn)LkVEfNg5Q+nS_R`7rt>>wSIN<48^)eHJT1LZ!%w9JYKI?EuS z1N!9%-<92MtyxGTJxlI|dUba(pU=W-YW^?I4$4X(9K*PYuju5Dapp9;6Y%wg<~^9a z=QE6*wP;9&TzUuKfmgkY$q;1$8LaX7&lxTWQ~+oJ@VWtp6^U%2$5K*Ciu9Lqct{)fzAnIg-gJ9P-iQiVy0+Bf}4Tsfr ztkkr3r-EZtVzufnqc0TX%9~hB+rxpf%s(^*E`^v!sY5j%4k{GI;`C$_3ll#4#{K&) zy`fiL)UUga(`sp+u=vdPaE9URuie%aqpgn3VyX=sYU(YW*zVoO$=Q#yCc#%|^LZY} z&NoN~*C0imFMO$5W6GBm>U5LzW+Uef1V9VH*1}J?FSah@(Z~-mqvnwh)6tkqLm+cL z>t$3Q4e~X?#aqVX9%=!92IQzj37uhp&v$N?xrZqF*JK6S!N+h&2p*RLNhSsXCWmQwfL7C%94@Qi|I-5KGK}tzAO?d&V=< zKs|YJOoKw&&AomqXi(|zd2f#FOxe*ZhoZT1>`q2s7)7Dsn*%tHr;0fWP|;qYF#gb- z7%3!SZMxi%9qEsb_Vz5A{&bdx+A{Wt3imd`Y+GLWcRn5X87kF}9^6h9v>sNoqdOn{Ye6r3m?2vESVNmSNUT#wOJN2N^8|*6D0Jo{7s8HmP#V{mi2;8=-KH*XhWsw%f4art=j@dv)x{-tM<w*iI0fj z#(56EF{#J&5CqHig%i=YIODTwYqcExMxCdE9kki{)0SarVi+pH586}R`YMK?002zh z_;yV}9-;+8>bFf(k?YGhw(8V+(vtET#?$_$M(9mpQ3Mj;NkM&^W>cXC1#tzq3VLbf zu*w0j!y!w9ttfD0YL)Hr@wZBb@3OJ+SW%wveW(OX(h1pgoV88Y5(#P8%O9^&Fpdj8 zbU5x*tCl*2VY}|?y|bXY^k->k-Owyq#%0WbkSE6T1+3NEV5JvhQK$o2NU%jhd!RY8 z`kS|lNAi`DKJp-D5+1od!~meaSt6f|Y`CMa!9n+bVEIj^qGvu@K$xwQp0K??!*xxx zoYMFo8ppCT$;|e@e4$cH{qMnUvRm^7GD!ZsF25aukH?f=5OcEVsmkdb*zGOHdf@%Pv~h9d zPt-T#*pSf9>}e4o=gz)omYRK^It&d?2E$8U9?@iLyZXZ2-AqVRNO$hp^j-UT`*nhg z)OlMg%)@GoCFi%q{)bu&mQkH(5pFGIzq=f=&~-2H9MS+!2c=;B6iGhR@`J8YLd=x} zpfHm-!WkX#`w=uc!Kd-id=sPFXr5qu{#EO+rTe#zd|MQvS41Jjc0lzUN$ zQ?cEgWJjHjY>h=~g=Yi+(e5)OKMaXc_)Qa!x6>EhzbIhJ(2zDXBb5WphLIfSgOB7P zy8svD6M3yiEzg`hN7f0rPKE_Pv{JCMQXiv+Jy0ERk|j|#88PVNx8Ca={L(zwe5J1t z%^geCiAzf9ZrG+alpcrD_fgEcRz&CA9*b4LP^jtO#X-00lPoK$$iB)q<#ff=Vvi94 zkd4e?3#mb>B#Nb-ZvpKuG9&&6H~eZ&*0Eaza~IFbY2_wFi5PzlO;? zPa(qh2g^2WDSkP5Ns|UO}B*ByG#LdZHy2tC?Lcc<;;Zb3ku9 z^U`Ola-zX4c9l4B5E1jkqJAZ`+0l72)n}Nxb3A}I!R;UmOWB)=9Vw5+;aMVv{Cuk} zPot)yK9j3WP$!U#^^eYwqglr z8r_P&AjyiyesmBO3=ApNfvCN!4M&NJEA;i~8R}|ZPGzQlJ|G-HND)kMiAF>->f*M* zmEs`DBmhf5w7*9W+i3*sER~XzuC<&g&mHlq3Gf3qCSqYje}ZA;ZLQdMg~3%C*f881 zy~I#vX^rVUq{ql|E(BlkIcX_MB5lTB6&USi)DfosMsYUa0*1TJ7)23<$}+CgbH0%s zCHW6fF0Gv+%SnvBJ-|iPD*#JCw7==A2$GtruJd@aS?Eb-@GdjXfRrt-pM72`WPd_4YC9aZ!r-@dN zjg<7o2@;Izs_oY;*@Fiej$Y?hD`hNYBes-QrT4B<+rr~={o!UC1;c|ictzhCz0&B`i@dr6J7@ zDC);*c*j=FTu?SJ1DB4j8*tVLlcpWGPLY}uLy4ijknw_UcRISyEfdi>k=2fsa zd!jD~^DQgzWp4^$afZuvS~ScBurs0_{BJ=vP%cg$0g((Fy`6u6v_98EWlGf|jnZ@%^N_=|weavazq z8Sg*r+f|}F%&XFaYqtJ^J3FB*bRTrO0r!S+_PJmc-b@#!y zUyByAP=GD1vZdJY&la&&V<+DH+W^ag7iY!oImHTAmaY(ff)wdF^|^SGaaP{owd#@P z*^HCVnn%^i5!9hna&7%YChK%~@DvcVQ{{HX7YsQO_OF~$(ua!tJd6Wf{1>bC!2)!^ zor^HPoSd`fXL_+is51-rA865BFqj#UgYp<@04ev}wjKfzdD|PxBA@1A2B)}P0$a}* zn1kBn!8@GeDs61=Th^a1vI>ujC|$`TPY)cg^&t&m@Qr(u3^vW zC4Gc9V6{4Sr~+iZLpuQ)fahIe7N$?!K=6Am3|TdJ=+xLxJ{;svfi!_lnfLJ@e1KO3 zo%gAW#CqO3gagh2i$GKouBV$U>9`9Dbo!xC1NYc1S{iz&`k}o{a#oqI1QWr3n0-{e zns{j5ONYUnas`_3A|KSB58N{V|k)#^PRkuCvslc=xIfiaOP8I)5 zD`wD249g;%1_>q^W3l~!Y8g6QbxOFzO{_QdkuzM~ChCp4i#3oN=t3BuFfI|0-;E-g z@{BNYB36895^5-kO}4slnB$Q9$LaV-ERaV6|JJstxbzooC_$v=GNe}q(aOozKb0@e zmDQy_YAY$KULm&iXMHq$(~|3+hIBh#GI};Jv6m-A^gL&DJ8VQ=bFf9ZnHgT=#V!RAzRB!@PK6;Fj6q%;u%&<d|n)lG47T-U#_n`m6((s8&~}l3-R3nRVePwGM>tCDvcVRXDr>B+xHVD^*n)W~do6T_ zd^A4KiTkkU@6J_Ay(W1kP}jFR6v7g!$5p_-0@j{~%@yhb7gcNXEAQv(IlM_( zB;r}+Gw?lCi!xF=RP-V@s5QV9K-D`6uhM}ryBW#%mpSbDqf`)HRnJvW`2~WnU5(}? zfqn^*s67j;XE9nIxfOtNL3$8ZDLcAaozGo3oo_$3waveGDo5LM;9?I;Lb6@3^c@BB zOV<|7&;f+ac@9p)+I0q-$t35D_AO-0cn8It}`f0R_ln%R}89#w5pj>aF`1?gKz zqvrqL{nVHH&UeutHzy+%59ZE<#R$r@4s9f;ofMEry^CqB6zZ<9q(E?k;B{}cn@4Va zNO$Ce4G#>}xi3X6P`zWBN_X?r#YUmocC}=Xk5BiMAQ%;R#g2H(yOowiUxqR25^eVht$4Z8$|FP{vm6XT z3~YA~f+157f(T_JT|iR(v{c$lgY?SO`n=PDMX80HS%aZN^Vnm?2n#N$ z&{kL*nUt?79mJNx{1jL(TdL`e^Cm_e;9sx=)8C%Dwl(#3cV`TR=6sXEib6FdWJ~|J z+A~{-<6iq#08OJak(6~_v;A@j;<-XQ2kTcSH?B6=kNBFh*$*k|oU0p?jcJ<^p7HGl z)y!Nxtt2PX1o~{$7WDkjE%k6@5?vr9+5OF13tMOhdM1K zaRCl4hLXEh33XYjtm2lwn{#B&D#VoUVK~J8kr<}|kK3SpBZ9z?oNJ}n=#2C|3C2VE zdYt3pP6n?uKCA3}y#fgkFcmA1LQ;9Jb1uaLh$8p*$arUDwK=$9Gp1!rjs5Y6{Eo$X z2h5_08My?!-bFjZ!}!Ahbjfo(S$+n<2Z!<>Nzj33ybuL^co~q|vKxM>+wG@y6Dq`` z$erojljDwJP~ay47l6|#hNREWVh2#bpxXI3EG5epf)ye(X`WK7BmAiS6N_+=KbCSj9t?^7;HL#g9h*su7 zvvbgy0)w&I`>wYji|>(m<9-$!|H|vxWl?{CKMJ`!YKMiQ7j&*#yZ3mF26gAv1qIu8 z@0ER*?A?$_Bgyl|4p>M57oe0oOiszaL!&JxCh@;)SXXhX|MX<_JNy^Cwatg2&%5rs zI0zuIR~+uTm;dFC=Fp-()M}^9yKy8H&l`CYF`|Io!-G|-g46l#Q?e=@7qvXy5V1s| zm=v~HF50^O3DN>pt#eLtPUd{37jgg3+aOH#G@w7wPSTsG14%x;CIzC>e zzfyrFV2&R~Z%g8QL7?|$5bXYRykcvvKV)b2i-gHG#8HA(@Ysy;LHlL?0uwDkmi0hB zp!Nx|;aM1&?Wc#?*lh!|1B@|^5?{on`3nTr5?rk&whDxO(OW_)?0mfO4YP5CBiQ3I z$Je1y=J#v3@MymWlK$tF-Or8}25lbWi&itQJFPDD$F`7Diy@jAgJ4oG8< ze;m(EOA%D2V}@V2Ptdw?F9j+VfNznn>Zw7ZzM#eF(9FFcKYI4-yQTj=i;wQf8zxL&yN`ZOs0F|mPcn!N+uffUx7ZYIQ>>TQGb`!+V=HD6PT5$W4*$Vm z-1QyyZCw6YpcB(>i0_!RP?%amOV>6+lt4h;!X5IEhVlHA38S_@0pQA#CNVe+*Gr$} zX*MbIHU(6t%N_9QG0?#}c#GY+$?xj&%Yvs=$rTfVCXH5^Yz}6V4oYgE-U0c&nWPFB z64GU%HKSG|WHdoTCI$Y>ZnPrG9Jk6T}cdJ zEmeRdl)6*%Tbv&k>LwSNjL6%*V>(93%k zlO~~`w*Y5wi(d=|aOTMRfTCZaQ@Y$Yp%eH}m*@I)-0a2pTJpDDjH>cNBnvYiQre(* zC~kJqhE_2V>ZAe0pq+|q`}wIvulfH_uC7O%y^E7RYT zA*w(xAoie;jgLc5n4vXe%W<=7I1#?48305G8lzbs`7LUK%K&)b@iI>c0zYb3HHKip z)lA6)#%1Bv(iy&>X!7VyHg-mwx^87gZgG}ELKtRk33Wr1}teaH>EE~sJ_q6 zlhygyK-U9d6Bv-gujfht&A4WKPD~5VoWMpXu_%RU)&mt7(G((Yb)g`_Md9KXfEVEb znZE_QX>h)s_~}v>20-{}cUxdC;v|vTwOaqZ!W5NXHfBkq2P+nu++Xk3f`k{(q_{G>)Vm=Vb^I#$j8uLbelUOP(_xz0_h# zimh(WhiL8G3Gak3ILD=N!z9hOxr5O#I=CHaxh~rAZR00j`WWAv{!c^FY=U1LD<`Ot+(xlHM{}dtK3F|iAY}z*j-6qg`D-sEr_z! zXusJ*gAcFRv4z|JVdB% zah%MePx`Mn0q&SNxlqg7{4ObOA7PcpjM2hi4b+74j9KM8cR&L{&|fAAuM5Sobwoji zqH62&$d7g)Fh1)1CKsI*(nB)|cY;=r?yS;9Ek>;XO4dzm5Az-1X)W zuOBIUc|WiUAQsk{b|#ljKylgYwR7CWYfPlIZGpy$5Qpx#nLOLccSPo2R9S2a6QoSI zn^?uS{M3k?Nn=_F9P{@Wm1XU|CT@NbQhY4=Z@+Uo;IyC(+wb(Ixb^6T>UE@h=uZ_P z0cTOe8o|K^g!lpxt7FH@>a+`IJKtzlwLH9;q-`{z)MCFlW#?^Uh}Da^qVXwviEiB8 zj%f3Z4#;NZ(L|K-1{pq|)H4v7m%Hqz**LM>#ezGmarr$HM>4&HkaF{rYHxSZbm5$&EgY% z3BmASWW;XuTnk=Lg9MW=4BiW#UIs$}CRCoz94XHTV5rdgZKK>Q9hi-H8XXfbtPSFt zB@Amw^s5d~3aDMG2ZJ6jXZ@A1v)nbDs-hN}gB~WSV*11JLRAB7oza;O^8EFNbj;&( z!-!Sv7u&$J8;YEPJN}Jn8>AG!Q1P$Cy-ZWU|$_HK%p)7QXTqDfDuN#mUt* z0A1)@793_R|3f~pw;}JzqErdrvAP}4$SAbADf z5Sz#m@ME!J6&?j?pEJVKvyNNIB*IjEMDUI1m7~ z`Sw(hNexmUqO;!#*C)f(5v0@7G%H_Wb3mCt&~H)~ zY8BJXO#@IzZ*9{VGvZRVsLEz7y-2$+nrIg-oU#-u*{Ktu6*%$&zQq2BcarBWEe8rf zp56%w7Ygla3iB^!M2=0#t*R3&^xk1)%Yj9JJM+%^JmE8@+(a0RIV{$KRw{0!CyfwW zEBPb76a!z34QuXF(P3CLNYaGVY6h1 z?#5f_hS)`~oCfTPOve}j%ul^Uqc;Sw{5sSyMd%5Tabt~r)?5#Ctw#BG7v3Vb74rBC zW3S$<+%nSyy0ZF9P(%DGx^#sf_!wTbCT9T3`!o*SsbD3+}w0N(V<$+@gwXEpjp z+7qNT1n}EN`LX05@V5{9Q*bI6)ol)9M%HlG>Hb8W+h%4~N?oAln~Tg*2?wJH2g+lVf}}+QRETH$v+ep^-A~*ScE) zC6LfB&8H-VJcaoDh}}rZB_&bJwLZ~W{3tQg?n4unpa^{5TcsvsQu?~S(v2!iCCeWF zhtO7{+KnO2t!k(nd|jfZqe!fW8^DwHEvp0 z_y+LvgJ)Vlf1Mg^d&mLS#=>I|^82ph#dDY9HMDqKl{l-5K{^>tyY%>Y6f+G=WAFyI z&tEQE)jbftsE6byC0D>H3R?kTBWJl{k+xXDZf(!nitU5%>V#_e0ZdH5Q^q_eG&YvRf@) zX7^87#^aH|-t70$p2o4j7-GPVc4(=S%y|UVgDn4!z2gJ|kPnU^<(B0I>-atqinZOw z{hQ=fLp99ou$phI3WusV{X~8#gk2)Qrc4&c^UVK9KjqiFI_Q@Nbxe!F_V0O|P;DvVNc$o0r;$P=rxRF8Q z#N4;3m5=wCDG3~D&k4g4SRN>R9Ohk()ujA8K?Rl4qeC~7(3^gA9%e<8Qrf*iRI`)B z^N&tzRrN>DFpejueXRPH_G=wvPH0ziah{*WFI7L-(vMIlHXZ`-yt5bOn4J`Q~wO$%U}&g zD8gx)^2M4c-N>Q0NaC&rHd`{xTeDX+N_Vp{^dOQ^Yi8UjNo z9qRDi;cNo3Vp?+iNa2NNBQSJo8{SV`hbxq1;}#Z1lv1sk4RbAh!Bh9B3Ic}I3D5=u8>D(T+D~UK?xog zHtEZPn2>&pe=pJ^Y3(l9vdPR#zpF|HrDLnHDbK4^`SSgr{N~m)2Wp_?oV$YM24`LS(#3ynIn)e}R2QwUxev+}^ zSHyS*Ka5@VLfD;~eVnGcW)k%0aBb{X{(F-Lq;8ZUBDhFn9S`$Vgz5WfPq6N_ z06Xr(K}0!gAzdIHaO2Yy^`@M6hHHEg157=^W`4(#S=W?_{s4Bd6=5Is~R zGc)K*%ESCMOJ)A0;zXh<66R4Pt{!wrp+cL#Xot7?NLp%_e{K{r?}0wZa;big+ajet zF`@b(zsjEgsY2t`(ls;l*g6IJlz#)uRoXu4;ra=Y@Ch&TmmV7Pf|P(AEEk7TMwooV z+-4z3^-x;A*U+}zNF@W|xm4Zw)MZu%pJY8R0*PNj({O$WH@7Xu9xPwhwMV13G**RI9G9356cKw~xNwN(HPa)bOdH={{9&_KfmJJ&a5o7Z7 z*h{7&a@M^={?0MprhXsFlTWgA!ge*Mp900z#EEom`;G)mA}=OE`v1S zK^3!8NCv#2Kn}mzJgH~{Ks6ML{dIvo(}${6^<&4p-%b?xI6yt}4;BI@4whR-rDb%0 zw~BE)CpM_UBr|@56Ny;yP)i4&tTZwHo0t!>j`94ZlS7@Z0G~nzwSu%i_SuEyVmD;S ztUA(`khZUG)<$Wv#h`b+5q;Oya;8GFTJ+QMZ7PmZ&6Sly$u`ole(0lQ6=T`*-YRtf z9LL-En{d*1*c4r^(^+pe{Ch4*(E~=w`ZM<-WqX^gMc=M?r!g(xUZa3>K#o`b1u&O& zKUh7xW&!*v1xN)UceK{y!JcSEN73Ny6v)&Sy_`6)+H!~AAC6`w*76{|WuC&h?djt| zP$wM=IYh*!cF$cbsiGFVJ)Xw1d<~v*4^cs@gF}o7xk^xt z0dk}Fni2g2^t9I&TAwK+EcxFV8DO{pXVLMvC`rl)R>y+w38F8Y0>H7gBySnRdY10g zJ7O_{o~85BTcGg=I5I1U?&V436KyF~0EWB;A({k!99Cy6<^*~;0bXh&m%aT#E6xk@ z5|ap;|9(9l!LykVZHZ@z44$@9Yeq`?fG{&|Kc>>v$9e-X5Yd9LIDJ;+L4bz=nIC@F zPopQGoIsb!ZnR3+4q}jRAR}Cn!tH!xOCMMy!k>=c8M?OzkBnib_|=cK!XY_vk%zx- zm5HQ9^|?4wvu^{g3UQU$doPZDVqEsI@&^XNO=eu9c(hTl5{oj@iaNps843KEK$4dw zYmp9z32@i2^tz?$jTyuL;lvD^1Xv2R0z9p_Ytjx7oBaNG-5|IxuAH=jgs4G?KnwbE z#)Phpp5tW4B|3W-@4-9zi91b}z>6%&j+9;IFduW-cnZ})66*-B^YGehE`t;%?*qj( zU2Yac;;7-vT0I#Gh<@LMf&n)f7)Ac!?AlC|*183Kw0AH!6}qYlNJ z*!TYUo?gIvZaHe(=d%|ruh{q)_;UX>Irw?4n#jAQ_uQPl_dQ{&Phb#8BLMZ>9G(ND z*-sClna_6!iKA!6W5^1MiGuBGG~Ton()|-#it4N|00XnlkmgiRVw#SZi-3?|Cr*}bA*ELFN zLBMrkmC`r3jMqw4%Wf)xBh70p8gA!1sfJWZf!H+CM z%n|ko%dw^soi4X4hMxs9v@WD#y3cf1%BYe8z}>Qp`@tFP1Y9N1fMwC&=oTdQ^yD`& zNi0`HN}{s)IxR(?#lGZHjBBz&u3bMgRylXA1LioC71Z^S$>octA|rlsqn-oreyGi2 zU=K-s7g0J-i($A7X10U`kg({r5d3aKS!FQ<;^jD*2N(7b>6`Tc!Er$K55)Ql4!oxh z=B(`})NwNX=4b_1R8LkK6V z5^4!aVAWWt5KkC#AgO8Fy!8b?zi_=MHy;y$O1qB@J)|Vg=hN-_X=OT#^^s@LdGUSO z(BQ*F`O5I$|H+HOwFSvnM(xUcwEPh6wN|R`MnX6z+^s5MJgL$4Fi{bsvZwOH`y+n2 zFe#G04k$y??8hhDteu8ii!V5bT{RkNHvxhgh^A8=l^MsI|Bp0lsZ4sF8YuIFn>Upo zFG+-(kZgr?J@__ba5+-xW=wacChoQhX@$n4zYIS+y?S`Vr;ylh)K+asODO>%FKFn_QYej5;C_bwd*Ut#<9S(odcPdVJX6^O zCWW!6L<62kbFhrkav=O{2tgZT=J|$SJjRvmw?TJt6r`XlT}?Ik5~CxUN-x@!&KP}j z4$NF>US2GDj*zbM)~Eug6vFbl`eUU6!sO*s9fl7*11RZc3rwRV;%2QnYDj)gfkzp; zHe$ckgAK?#CfjFz9m#FXG31)_1?=q1{oVrqL;cW$#)DaLOwGv^#5?55g5x%2tB%^6 zWH)kB)KXBfA1hJ=-!m)>=*8Ns!e|Oa;TVGt2Q^;n8OK8UD+ChdgHAFjk7X4Gl^MVc zYL>UPVV7C*!9{g*LJX_Sir1altf)3dz^IX9Pg(nowRX;PtP_peDZq@LTpoP9uBrxs zqX(car7`&sc(8EUTR~J86uX*Hv-Zb(MbB_C7NpvWP-I>>d+IppTFia-MDBJehN8v}Y)vGxtgmS2A(V%r*e(PEs&VLILuolJwDoDGtM{{7kNBbFP>h&PhdbR-D3P zvQ7z-A*GQh*TF?Mj}y*0k1gv5@~w~P_W5n6)$fe}3RoFi&Rv9=0!*BLwQnCh*=p9g z?fZQOw1DnKly!Z8ijPC?BhWK9O|z+@cqNR`#xuafXvY3dfwW(z*#Sjvh(F`MU8eCe zemCqk=#*sWtL&Qm&pR@a%!Q(ha`% zlPqAww2-4P5REpbklL(YLkIbCkspazWM~%rgWi;)BqU^4QsFTaj$)z9u)wBx$x#*! z{OS#`a_MSSD<6|64s2p~i7!!93yUd_5iWA`Nb{1RqzQZ8Pu@Pfli*(nzDBS*wcAK1-| zV@Vq(wgva*8PPnBlPqf!k{Z|x=;1|VW;R-|kadM3X)|3m-Nrm_K!Sxg`-C~F`;mp; zgpMAJpC5h?hJQbo8=p33lv^sdoVhiPWnFQ*QFiwT%lqu&3mr3XK{?CmhC)* zPi(>(iP-#6wn31=CcBe3{gb~hr<33RV9iZ5VP|DmU$=XJl||LAv4@hW#i*-)U8}6V z2Oze3Zh-=fD?`SGG}b(qugf?gg|H~6`P*TaNz*Vkk6&q{!{zIH=!W343RY?CA=gK0A?o`{Z=mfuPn2PLrGyLoQ6t*!rUGmI0}O5rQJPh`MN(3R)vR zJ5X3iDr|}1^zabKAEbQJo^mjdRULi*#Q*G)>%0neftk4v3+hPL ztQolHuBj}bW|nSmGbuUG!aJBPjbADWyo8*|m#0QdWV2=@xw-2;bNESMK7`JQx=bEw zV9r!+TzGSR!k^;M0OoJk;rq4ctp1Tn8Ha^*baeK05$8ADo4eMSok<9Z;mawybz@uh0JrE<@>`=O+>Prxs%sF}VrOQ?EvK4RqxSzBzdqGY2cjdK{f_=wCs3Q%-joS30WY@Z@B-7 zWTXvU)0w*UJ7%U=IR#QL@A2+NYS=M5cmwc14FCe|#dJW1BGW(9-2%%bz=o=(cGt<@ zxmt*9D1BVBVUP}0{VR@B6c+DTYYvSI7IiOJ= z$|cw1HIoeAzMdw18aOsZhkaW+-5;+>=5)|qv-@$un?CGw3;EC~q1MK_!5bg>;JXar z@4)E5O@|e4?o~Te){~V}Jfs7o1BGybHw!m{E4o)?KPxse)W4`2xrhu+d1M+7)uz*W zv}O`v{&yM`FbpLsgk23P-5;%aqk@M9&m(EVSKi7FL$fUy@*@>seibUZSfT=JNT|^a zaUqRs6Au~8K`me+1GR7*z6%)Ehj4PEgGUFiJGd6EooH9>-aiO<3iu`^I_lFPWNYP-q@g9rdy2L5F1bt0vn>pkW_QP*In-8 zf^^-ZKV<2W){`}p3jI7X#Js8JL%KnR@-P|X-7utc;C{=9%2Op7uv6xRAwg_FWH64` zlMOjAG8jeF^{O~sL|Uqz7&XH9u?yFpwH7EHJe`%p-bT3 zAIllmQ#F$a?Q1`c2v`USBG}kBwA1~mnl~b-Z#9%Nuzg%ODVGu1b+rsCHbA-#9@y{` z0%MggWAg*7oRPWWf-Vz5eJeD^K?WWX^Zo!b#ElHnyF@=D%=IqOe$$4-KWN>6VN>AJ z9-6UA>!F$%6zpp|iw*Nq+nf#ij&`~~RFgD+sr@w7U8NhdHCTNc>9D;S!|6~^mu?fO zB08{e6oagwz=wXleV`zjOog%q@&P^V5;OUbz=xil53Yv;J-~-e4;RyU(cvLJBt{41 z)(`Pvojc_K?s{K zAns#V6BQES7VfD6At;C|oVyE+r$wvT384THyxy=0;=*OUp}vm^x7lcq{aV4ET+(R=}Ab4ozm z7`oQifD(hLCq~*mM$~wnM6hdKctHgvXe3)WfrtG_s8x>Qlu(5__@U9ge-+YilGOj< zv8&!miENLRsMn)5lMwl9K8q41xT@Z|-Jrz%q;fbVc)M^rzXpTI$q1|<10%W!;iCG% z4p!HLA6vlQG7_R?Cq%j4M^LzrfNC8PqLUD4VPP+K;xjHn6t=Lq6bXbFiV^wdB;IfC zS>$&|#YTz#juJze<$SLnx^S*XZYCwtiIga=tDixAf8o48bmIaTb`6LtDS@8Uqy+Q_ zpoD?|Z~x@PP>|>>MK%nt>f9JKr{!^WC6+AQN-$rjYr(M_R2y}bKmj{@F-n!{KD_=g zhEw7sd*M}-*dH61L*>fY$1;iaAdhSxE=jNs$i>5mZbD%iu$<&WGwDf^>*s zix7X-1}d-oBdG_3e`I^YVqimdfFJPU1+1>fhG38NxYuJhGdP5=@hmpf>?Op+K1_|(is(0`~bilHaFnA>(9Wvg%7&q5I_^@83l{M&|k-d#Y zfn3QtGC+squ7Onox!#-e!NbF!-+PxIbDUo6C4eCsbiVbm1Y$jP^EJNy{2ET9gU@GL zyV2slKXmiP2Gt={hUlz9Wu|i=LkbGjS!7@v3!9Fk$iSiQsF`nYA*8E(vPNAol6SHA zG!_o!VIy9mBcI4%MP$%f0e4@KA)n?h+qijWiw?zp1&6m^L*_{`^jaSy!#bM`uRmXZ zo<#;reK;F#bhz)&+`O?tXNB8$b>@fI$(%`t?Abm3cwns=Ds|Yd?q>x(XpZosY%^M+1gvf3YO+H}v9Qoi|tZ{UJ50!MN+?Ctl ztVlX^ZrFrKONR9CGY1Y^6pU7g4(M`o6JlV=I^>NERcxrvhU#>vpDCWj;%7z&dn`Dt z=WZqwzQ2AyiwpBM?_99`NpXMdCi~cp%!X@H9@%Nwm}uApE^L-7L1#teUU69SB$0uq zI_?`zhL*1LhN7!x&=A(3IIdKZ0pt8dQhKnViwqJxxR5%x_*>f-AHHT{;je9?&WA$7 zGLM{O`1bnkG&-0o>uq0n$l>ucQrFDn=J6&vR48I59V)LGkVujai1!g`Bh7|epee`v z>uwqdY2}qTuCk<&nTCN5tX6aN9Y%gpwrs=Mpx9u3Qe=+}Hh!B0&>vb+phd6Hbe4z0 zLtYQwOg?-)j}JYJjSu@icfLP(v&Dz1dxf7AC>_V7Y3^)VEfRG$fDbfF^K0iM0XL)K z@8{}-sQG$TYNd4q1=wN)a|dJL0V?GgQ-)JwL{UQZYV17`CCZ=OS@CBqMNN%ZlPksj z0Fl?DH#0oEzJ5K85Y?Afn1!?_#QkK_>ki)H%+3gm+Tk58Bt%mch-z67f+i?>=12(2 z+YnGZmn_%r^p&d$G8-MBLt=z5o)?+tMf3jVFtNeUi(<}(lMW1CEn?x-!g!yUv>Mca zz5g|@+7j|T)bP&h(VH0^#_MSUxe;7jKOdYu9qy0bywO2-3-^2MxcEh!43&K7G^@wP zQu1NO9r7V_o6+P$uF>-i@ge6cF-+yQf((}_@qz6uqW~iF#4uH=O&YoGgL%<4a=HF# zH=sY~7-!6Dq2UT_H}8KKK&_4C4oa(P>A zW53yV_))2xF<3#-mmgYZlGo!mlMns%G=g>AEY)oQVrryRQZ_xWb)yc%QIoQ3)md z@JI(~A7a~rLppm^Wk*tuhx%I2qC<4|VcVO)y!MIyK(Si06;bdOV;LO;NUh`>eu;^m@c|G5=p*$QNbk+3YB&}ajjfMsy zLt@UWCVUG?2-Iuqe6zCnkfTO5B6p)<*|Rnmyt`{!(ZwS8abav^aWx){e?W-fxnxnJ z+b8s*JJ#o9f05c=M?}1QK}vK|BFBeRa(O+CGs8oAO=nReLuDpPm}ZltHw{Y7hr}K?#m1Rb`jpM9DG(;0|7JqFiq7nrFgL$BY z8W=vN=e6pTf0JWn(?U@!|eJ zPIkS$>8i;tL&tk09i%BnbdGpqIG=>DC_-q1MN6u7W8^nDA#%Mk|1c;pllCWp5cGSf z+>pmN%P*KDgfl{f2wRBwvqHH)cVuAEJOvsL8C*AMJBj!OX>HI4}AyE;Lf1c`K_IQekGLpaFIL zpn>`pKBt$wOnq}R-fvRaw+9MP@NI-NBlO33u+Agn_&I)_#e)J97x7?UbGSc*^F{+* zGx_LtYQiX(XW{_q7@x`FYDvQ^H{O-u_lh=jN??$JOo-Pz42(`>34?lP#~W?#2c~a)Mz4A1;Av#lh*d5M5Sil?;tuG* z9#+#h4(Jf8=uo(MdC~#)!j}PpYm^d89pXbH9}4-vGo!dgwRLs6gOm>`ZxJG%U8ugp zhs^HlXb&A2>*<>r9KMckXAxpsdt>oNm-~U?n~e{;XExD6Q`+2!o83Y(swDw7&{hxm zrpS<5ar0u24b85~k}c{^;Z%scQp|`GxJHT13~y+5h6YamkO~sPq|rWR4D{%tJWn7J zeX9aSDdrbr%240`Qa%M}w2jBIhxPEyj18~j>uF@LO<+IYpdA|?7`|~fI2p3o!1>HD zpO;EHR0-6$cXiT1eLcjCDn8^PKCorjo#{ls-iBm|R(%zU!*a*?_dLK?Z| z!+FUya_ML|Zyg)#H`bvf_RX7-+#N!!$8Si8*Ru%GqA#|vaHGNf!2Hcd2wgCmCl8P? z4Au`p2;)S>m!g_Oui`_Ceptv90)m_TEwFqcpbxK}3=Y>HB9v3l5L5m&jq=OU> zsb;4`{j;~tKUv zj&&nSk_&@9tl?V_CK;NqOUl8?Fi*}aG888RdRVy0vOZlh$bcJtrYuop7$SqGk)AsA zc*91nytjiXmv%7s+&?}nu&;S*wLQdzbsjlJJCO^ed&45!=x~pR6UnZN1q#q$13U(Y z6m63ZLp{yhboM@5Kmdao5A9Dy zA7i6~GKc0x2lLFKmEYk zyrGK)9jYfKDqUq0?I!2{O4^>eUKJ;OB_%l63rc8BQsZ8dNr}ka$yb!%K@{2T0wpSY zSo6ySz8~mrxxpYVA(cu@r-Vc>$=YvHMBOV>FOCa~B!snx#<9wJ{$>VIg_LsyI8>4T9E82x+Ur~|JS8Kng90nbQ+Iwgq@nIWXj-*7(k>tzPq znUi!XfetJ@7>|p|4D>VHT@WVNWP_gzKU_kMImrOaSdf8}-cdWUz=kX~^x^|Hh8t7fgg$+6u2h+{dr^j=JVEOl59-ORu+(LId~=DFm4 z2U58|aLMS|r^S6z9&6IOJs;C9u_QzW9?%&F}L<48pYcN7%0F1ZkO&x9VD zTwvLuvFuP78j3p#Ho2C!D#Zihy~vQ(Q+a=Y3?H7!vq7;I*2h0^%Pi}un;96wd1Od1 z)Dj!^-R+Vd&m(!Y(8U5;KrFZ#x@0y#C!(_f3n`>r5F0oX5jCn$Hk9jK20e)lsVFi? z6W0_P64*f9JlQ}sRP%|v96U7KoXGo2j|%kQP&$1qHms*^M(jUFIE@a~S6fVU_eF>M zb2m;09uyWNhe{Bsa;BDa=tY+b^rt5sQYIS)kA>#;E$D8_NpP~^s(ID3ZWPhf&FoAX zc%jHVCXyQoHdAEqdAuAwGTdC3_eZMJ_DA*^puSEfgxVgX!a9Qt{YU?K78P>v#TIqO z);$&OXOJ@du#iOtWsnIEYNtF9WEh%;OG8D61fLEZp)`YP6#$PwaKE18WP=+CMw*LV z#s(Fj&?E&L=m1kYOAeU_MQj@#JTye7!xkF;Qb(X0bU=L?4?ua04$B;}|LouUkJIQ- ztT`R_%D8fW;^vJGMRf3^;@zs$5;%DaI&>8r(hwa?t!?-KDmawtbV%0=4LV1=0!x?V z17!e>Kytq?UEzU8MHtS9M%hctsyAd8#--eTvw7Ty8y5J^r~rScw_Vm#H)O>3(-<*) zM6=FRe$R;enPj6@Ei^#Lo=LAx7jaNc>s7L%buEJU%meBpq-B2VZfT;5Ixm%LEaFD6NYR#;PQ& z%ai!fueTX=?sX1$yu}CfxaVbteBfKtq=rIJCLKID6mMds{J|FYZ{3`S1COQjc^P|* z4(p8aYae}_Mu+Co*;;QjxbF|&Y;<5tasH2Il!J7ropBA;Z15o!@`0L%$Y+(S8%YSYuzZ6a`XnSo{9W?6{>PNm*uKCb$zxsa z_3X`z4*fhrSoF;nwchA*-ygmy)d`^+r>YMy2!w}sB^^xrZs|o0I>6KkW+*Zvu3COL z=}@!N!QypCa|F?$sD%|1n%p2E=)m{U+?=|*JT9t-l{a}@EWcGA_wnOmkPaz7R*PB> z-b^~oBk?pkpwlKg&9S0B9i5aW$1_0evu-`w0DkyMEJvF7%A z>Si7k*4tTBXj_rG-so-LAG#^#OwhF=DRIb|Oi0KS3+@7}5!3AfRLG)2QF9x+lNA~? z%RR|ZuJ;(QfIZ)hEFE>EI1a%8WN3J6ia8e)7LjGO*bRG9=U`C)8MP zMKZvN;gSqVWJu~@!4(7}gghn!-^Q$@p=+^b3!X*#og(Dlx{LdR%a4qVaiQlU{j2rF z%{(G%ucvWgxNChabdA^^E+rHUx7>L94^u|`fB5yxpBT0b1wVeC$8>-@#}kG#=M(J1 z+CwQ~4#W>KNK9VNm)wPde_rRmi{^Ka?K57y_y>KT<~uguWPSC_H+ueaY}7+1;Q3`< zP5ifM5{2nF)F)7$sJOZ?SK!pN;ceuire}CgJkgPA@{FG7_#Kc=hW9~!Bbei9{KCt( zahdAH5PpVpO37vz{j>QtB2UlzF7)lenE!)6$=~Ml!o2$>c_TS;-v%vIG6k;Oahx;P z8ovl-%)=l_JAh(66LKCDnLE8F6dc7!d4CBy9ty$#r~Vd`8Cb*k#I-4Cm{vL0M2?tZ zFD#5fQ5c_3033ADaa9YJ?Rzv`k|G!kYB(YK3~$}57yOD9O_WJO7+av3GrxM|Y7dcd z<0Ng4>a&XbPk2X0WR;$){~G6z;rXwTl=KX5=E%u4!Hf? zJ!%nE;(VU5g#yQHINGC7iu1j2Z0s5CGxM_qP5|KA+pb^z^AmO{2i{N87Br1VA6yC2 z_M*Y8V*cq1q8qv}sK!x6jmkYpFdy!OFa7o-U<*fo%A6G854rsHi!cxb%Llf9LhNz} zq36^whrRcZski4ew%8T6;19;r(7O9})+Y0hCPglFb8ml_Nb12UD>&+URN=(<)$x{l zk=NRu>NVUwkqc849F7Bao#aU#j3u%9D~>yIMG&~z7+Ls`L?qhUD1!1X>OGkealSXt zFkpc&4f7r-#RN@96}6Y=0n0Jk#-|B&!k4Y{9Hdz>uE=EpIz4qtnUfR>c*48MHxtu*n)OTb=A z;onW#2{ylCULw7g<)4f+Hn9Wg(Apm9JF zb^>ccorj=?Ua=G=tK0@psOy^8Yt8o0Z}>P3YAaDt&l}Pn>T(b2#%1nCn^E2gOi5cu zoB-HHmZ==h2VY=QMLj5^l7ZQ9u$j<}Vei1ssDN=eu(9aQT{=X2h7F)i&oZDL7)D~} z8=Oj7!@WKqzvAO8xEq{r2=@#md${{OxJ}l6vV_FlM7t)mT>$Ho5p9EWnI@ouyQ?*g zrUF-FDhWY}>xFi~ztE1HrO6&gf{w^cw4+W)x=)uv>oDxC4vN~TEzw?!j!#CrbAdUQ zZ|!{$YQB3$nk>1$1SJ%-R0G(8T1ZDHHfwY;!voSZ!3eCa3F$_p>2DBW&0D{)ADo+x z-8u+#64F7(BWJ4%oWSSX0JXZ1j_Yh>t?B)V{lBkgA>H92MWmzMK>F_aSWPiH@=~TA zD~g7&BE?-aDH(}!M$fV@R2>FrGu~sXx`SN_cGUS;2sSHk*%Y%O@yAz*3?oS~8}CZN z4(JZBX{jA33|)e~J|1B$I?e)n9*UV@&l}Mm>~i;bEPlh`lr1Dp&C&KClA6+ypl#e) zbJ+^%K)Wc<=0;Y+odI_rIv|<*1-b}k&vU9Le_kt`(Fl0c3j%;6q#JhU5%9jgdaebj zKRZ571Kijbf?6-^Z{p?dAqg*74lH^g8QTrSGZN0W1$Al&xN`08`5!R_;Mwy(Ms2(u zz}tv7yF|Gt*M=E%@*yf{-%WV)Zl5Vf!`{EnXDY7wy%u}^Z1^|_Zn)SdrGNA8djsyf zhomprmyk@j zX*LPCwE!tcg_GeaU~CRIBnyO_(Hl|W7v9{im;E@diHBvyKg>+?h<75~nD|2_FbVFm zzTK?ViWBVmvz`Svrlg7YOnPtNefO+Paf6`|@7(=R8%j*LG$6oo(ec({Ihte!Z05iq zc6j)1EC;r()3T}l*NC@1Z#2P8wEeI|EuL_v2)J9}k%79`67KbBS$~%E;7%dwhBKkv z!)h;HVWb~wTs5sk!Mf9%erQH*IN(mf;qD&1ao8fWQC&)X3VQYD=&4}f zX<3zZr>N7Ci%JWGhAh^zhZwlGcm?=cZ8zbbAEyDXr77?SOl=SN-P1C3@y<}M#tNzK zd`GKCyS)iGHzG{H1-GYygb8przrBN}NNwsnxRrT(!3)FR$(OT$EjlcDk1edlJnsW9Iyy*_aWk2V%-kFGD8S8@Y~heB5~RepIM_NDTuZWkMtq!wb}8Kc4A|2RwCx_+z4@ITej`Z3 zJVh=ZOqxosZ(yfqc~~kHl(B$o{|Pp)I>xDBfo&aC?&M#lRGpQHsUTz{=D!Orc8{@H{&tt(S&1<(Zg$9YhjENXBsOE;kI_np4uF@zTXe;$obvsoNTW)bf!~Vd!Z`791 zp|UWAX9AI#U~f527-BDTznCJ?R-X)_5E36UCxE?9{nqNAAK&qD7T9e9O|avQo6Wd; zHtHtRX?K;tWG@RciZt4FQ|Yjw9fh_hepw+qw+(C_jdK5wMwL$C(U`p&GgxyiU3s5F zjX=f$r})HAk^psDXMJli&W~^SI1B2IJ&J@nrCqz-!rjBM2x^W7plKrG0bZwxbrZ2u z_l@B?AJcHGy`7M^B%3*34e1@$ndf8LN?$wyIh_Paw$4ZO$>^E-L@P=XJ1_6-w$}Rm z_==Cy&^FKd!X(BX?Yrk=VavDA|E+|Z3oqy&XoNfJd`zmhCKsJ>f(K}OJ0Wi=cHt3O znvO^+r1Ibq>Eedm7`2Pk4Wa9ZG|mDgo(<5l43hf*Z972w^~dX3XpcB_qnT7ld$jK! zkvS`97tSuUZD3w2iGg-kg(NnITS<2I*D?I)1^;^_I!y4cU}*NZtU= z3E<|Mk^*l(pk1K#;w^Or_gc9#!5%-xX>d1s?#|PEq&?j6?iraFJ37xbz+Gj!F7teB zqL06JX7{b`scr zQ^jZ%(9X=YLpH&k6t&?=3$6cr$LGHfYi_TlI}_~raTeUTz7pRM9WZBZcp_C+T$#$l~As5x5Soe!`ZH~=vr%{Jw61%0@_BL7K z;2WGr2H{R=4fpz#G<=)|_sk)>;GXB%9`55&l7V1PxT*L}n&_o)3U$5QG)xlYn~m{s z<#DKFyi_?au*Nbo67xL^P&h!_{DyYH85#XwirP_Oo1`Vcoba6UIqB_vRQ$jQS+YEYMCB+d*ZltG~(=Oyjx`CsuKLaH9M zGp{yC#sg(PP@3Ty8MncfuB#1NKSDa0Vm2=DG)N%QHLj5^2S}G6J*4|*IXlEQ@?5 z$E12!EOz~Ib?IMG{7BiObz)l#9)-Y~Rlh(Fg6Klaq~b@WuR5kSiM9%a@Z+DkNJJ_N zwAWo4TT6B(*6Bw&3+)2K3U0@)H^46Uz~+5%Fg610Rurte2N1I;u$|YVq)Egs(RgHNK=8edW(ke0qZO^TJb@I6ud=~c%PY}<{j3VCu7=VZ!G%p zma0`nKs!)rf`v2jiB>B>JFP3MwpQy*tmnsRXrpWcv~Rf^S-p2QRt5$XrV(_99B4S3 zpgcr{D6CE(+Fs5!&-zN!T|ExJJtkggaMKTRzRH}sEOa|lg^O1j3ctC^U1;M3#zYpn zlUPsutnv0G*z4ml;NvW?Et}#kImKbl#WC$P-Iq}>E&586&V#_0CwewHY^tY_7qtyEb_%qGPDbP2$o5^;L7n_hRfH--x5sFn^F`hsV|)j- z{QX>k4<8Lc4dD|Ot8;2z!3$WYCwT`0>b2z0)*2l)4t|^kb(0-8ZoB;k)c4NE+;l$H zAgpWHh^y8C>`#HUH^ritvzzihKOMPinozSPhN|fs)=g`tU0E0vu(J|BisPgA#3E(L zFkua-t|nVwX4V3oiS+#MEUY1kC)T-_zQ_9B@yI^QP+~WsHcK9n6+%W=2=UpmsCeDn zjIes+AQq3;k*6b)mu2i%MffDmS7l`V8f9RU}L8zEF%)Z?&})VTAeeI z{`URbX<&OQlFQ!E$=bccF%^Mrl(r9ddc`hol>4xjP%$K{0u{ZI-9vk4?iN&uCE5g= z`;Q%LjKvps_i2na?B@z>g=;(kfEqw!=M73)YgVs?ITPlu=K(#k={l3f-Yd!4y^~Qz zA*>TkM4GZO+*cSu;J&YNh&RMk8iXOKhmfMHV~ifI1Mf2-=-X31Urz%YTkZ&UEqk#0y^}E#gyA_J_$jOzLNM8` za)`IXq8G8d=X|`x1M4lr#%D9Gxdhg1d1=dvItEb^h8$!rQLC`d%0jS2A#uzDSf^!E z-PYQiiS+z92kVN9fIcQk5(eUM|oIyJwDqstDky{sCY*s=OGUMXMojzRDU z66nq_YWamVjE4(1ViN7lt7DQyPcJ$gkw})vwVE&!3j}+ug}1dj=lj6NSzyO#s$t)- zFR?q~oK7`J#IA=(sEZ4oipP-TB~@>PY5LK)te8-DosH;Q3F}O(6SX;wy~Qe*PM0u5 zB_h=Hzy(NWBgK9I8y=5?ep`VJ%N5vbZO(+cfA6P(4K)qHzHv>zqs^IMqo)osMFkO# zU$7ydD3ZOX>aDOCq7MeU@#)anh{-UbO{}G0&>#o;>l16Ajp8hC(JUHj?t@sX7Z%y9 z;W>@EYF?YS*jk!1k#67HSy-oBmGsR!z+S;{N1M|%!49yoWEbXO*&By`7vYR> z1MG4KY#Yt}E78@Z$)L+YhQPKYu$|i7J7HPbi21xhNbaQpu^y_0<&D~?_UYd!gfLpS zu9Gpdvk*80!jX8u3^Vs7%?s56WU|0*FoWzY3*eYu zb9$}GnNZJ<)4;|68_^DJk9NFwHgbF&XbWs9njqgxLfaHSx`XYIdCF&A4+%kcbou~u z=QY7jn&E8#yKjzYtf^8+h1GOA8>xqY6*)X*(X$}fC9lC=pN$zGXMtUFNd$Y|F!CPk zduL;D+x$`$K#I3-J;65BREsylHl<~Buu+E|U)*j4_M?Ehu%g;E0hVBt$thG1JIQsG zi8E4wI*QhH(BBYmFTS0&M!Fv$oxZ2jkRG(-g57G|Bc1M@izO)w(cL9t217`D6tPwX zwVDON#oDo`4aPqnN=gVe&&8I0VLkr`SeuKgl8!~XU2|lQSO*1cTu-5(CDv`J4{2*T z&O|zXk7r?RHD_WCqq;rT_l`wobXX2Tq*d*R9zUAOshkia&iSoWN1OpHN+=$zJMS?j zxk4|R+K2$!^$MEVNL-7{EhE5L4KVN+FsF#pby-_+wY3gsq8z@5vv6*aR$LJVPGyht zy@Qd{>cF`vdT>Y}x(hq7Rz06pf77;a0v%|=+&Nx= zdG;mJ%Oi2({Np^NyT^_hVe1~~dnY1C)UkC8W<)vp~il=^R8ZizwuP=4Pjn)v`Faz;2?2ySX1kwnBR?#FH51y+GZACZ zt`aOb+ND2vIf`X(osSYMz#$Z9vjvuQ%tkgEfOcQs(AP?wiS_HZ*R#-$IqE7?a%g+B z?H<~`+Z!${d5aUY2@=>cLoM!`7+gi0>&7=3b-?*kk{ zi8Vq$7M0If#vVPe#x%nc>-7ow`fYq0r(r#k^1eKO(jC@!gg8w@olqK)$yxna9=wrY zCoiY61%@#wcdGCTAEPa##f~*&w%^c|MH|k??xITt>?EWqbPsHQWqzh7_LMO(V{zF; zZ)+LOgt>p~X9106lEk^>zQ?)T!I`7!*z>|s^z8mvFv(T~NnS~1|0}(f>Wb-b#1=W= zoc(%kjMsP|IC`=nw^TQuu%kCfXC6D6qV~ov%eW6X*7=orQFcN#)cW z!VReJ9gSKmK7$Wx{8;(X6+cjR#?Mb5*5Gqr5T`q(eY02O1--kWT(7*!6p0mdU+K7? z^DT(#(*WwI6&E@eHOtGM*OP|c0d-ktA!`ZF1lmr7x_QQz>b|Ml+P!m;Q|W-ZDS9w! z;xg*d13N+pbrRHVb3+rsl2-nmg(@k03?0)Xc_L(xPk{9;)!v zd6j8yB!}Gxz`EKStx+I_%ezBeS>FpoP;*}hiXcMz36E3Lx|ah|ua89gR=?HLa2`>i zI@;Ld-0vNU97u<(92y%esLAh4#{sm&sd+f5-+XG#fgVJ9ORXsi$@v$jYvqyXGi4h7 z554G*bctc(i|?sD(Oe$wfpzD+t@Sq(=kl$bh4kd261^MlajoAw6FG~{&ABL1gKXKo z^oes-B{g>^wEM97HqLG$w3{~f$`cWmWK_Hp%}Ahqrn<|CGDxHVA5b7o4;VzM`KF)< z&ryQ59{g`>_00r2f6HeX)?)T0_&Y^Q_HIYtk%1n=^?D85nV*%7&v^LN3 zSoj!H!YAJZ0qbF%iM)QJ$`9GKOaCGd#UP17Riek97gC)Lq%)fX2fcx{@@B%EzNOQE z)}GM`^uCNb?j4AnLC4BGRFpCX$#g9@j~ymasxF3@bZ>9#O|({&HIU|R%`6--^CZx` zdKYwIy33<_Q1p;E0RpJMZBBm7-5Zd`OI+30cbB#BW&$0*#j~J>Me~F@P&2s)``&>V z**C|~RVCP3K!Z-2uvR5Cd-(8*wGAoC`e5?W+%1lq@{AKOZ%`xh0PAh&Mkiub5Dz*L zStrM(bUrE&e{#sKm363I%Wfvp;Y6&5kgogPdOh4b5*kfcp$0+QLN%e* zrk}x+5B(KnkK1$;Z;Bx+ig%rgYM+T8n2>04Gq_3qn6YL*ff1C!l(J>BtviGZ%`W@+wX#yQf=Yuwc zaY7kzdm2L7D&Q8{UbF6=@v-DR=$R-$~doqh5$c zTS)*r85#tA?HSV7(whnO_pc{)V`%P?33i)Q*n@3%z-}zSC7FKY`sOWOJd6yLQDFii zw0${^8oKm89C8vS*t{>o*!GRum~@NZVHc9y)lvmjuP~>Wk_Gk zZzk5?zJ5CmZAiI@cD6m*cjPyQvyNypR7|wBf2MLzAsQr__|I&8VJ@!t<|XBQLk$9} zE_KSh%mnTF8BFeC<_TH0O>nf}(;va~lVG!03$4$5n_ynWQG%YAH}t;N-%P0Ie`kS> zt;_`5#(pbGPxnqpQy5{pC0EXW6KjMKfUSxDoW*PnsW+9h0d`|LV(u8nDwJt+=>|4< zo;lnFtV0vn#5y_DrV|oHg2=f%K)p5%>1zqj#QF6+s8bNFZKhFsr0*S&+-d~-nV=w| z^)prWduVA#1(DPpYF|vaWqxW8@E)1`Dvs7?e+PAC%+5ZOO7}ela};QaH4CG|I2$N_YCSQU9*YoxYv?;nLRUQLY(*xf7FgDu=3BmN;1 z5n|1KY!E&m3IkwnYQz3wpGs{qO|_z%Wf9sXDf26VVFWsdaJ134uZ1|1*!^oi4es8R z`PF%2+reE97aYIP(2#I*gdc0Gvf@bcI6|Xq&2zrg>^4FjbQQ|66f zk9N6-cGL~BjqFR5l=?+XC_!SE6i6@%4YY?WxDcfQps52kh&43`D2&j)Or{vrjb!O7 zxd2!PBp<0S*#k*UirRRhxVIhcf|1tLUMq1X)b`cR0=tIR1$It1z`l1xCRQnu50RQX zHKXiS1-92#y$9CiuF${XMVZw7b^97?}+4NKAvuw=erCidK4)VsixuCNENRGs0^%) zQKb6n!5+EV5wdA49wOEO)m8IpN#u5${}z4WgtcYG>=|IN+t~>9dO<{At8gaN6|~bp_C(`7^hHuHFa~Sw=MT3s5Mhi-7k*K0jxJ2gt`*y2zlA&M^a@4JfsiF^mpf$ zW!%-V6(j?0%wPwu&s)L0mf}oar?2@exLaw;{Zbt_;6A=SR*s@4_+`oQx+q@5$zFp( z+O_SyuwXVwLY=a__%c&t>||`jnnOy+{+!rZI}Yengv%w7 z4j!%p?=!8ocTi{C)2(d&iRCGII+$qmNvQd_0P2}cVheVCj=T!3kX~zXCeHC|JPm1U zO8R1pJJ~|Nqs6Hb6eP@gx=Afhzd}C>&ULD|n$!;KZoA3>4q%-KHtiS)HZL@dTYVht zI(Rr{H()iEpt@x6@cR_l2wkwIn^_BX1vh}bb`I%lEzX2Gd<|!TJup5`u;;0=2ixv| z-AsogjoPzo&H%O&k*3;e@`hNC@0NE2tw0`Zwg_t;jV^c^)$^%3+u>8udaI4@HuapY{c zDyzvnTx*#`@{}(Y)`DUGu3UgO7X_B;CsaQgS!sny?u_4D z>q)TJz9D_B$eC#WI16oTnW{?qLfJQH`n{8p1N&HikrYF;W(F-t#gI<*LaI*dP=DRh zlP@G4zFQVx^K3M#7aVN75ff~eFJJ-9rm;-4IhN(qXyBl?}F_<{aiE|1}6{cR8vt2s*UcqbdMtH85DPmqw8Cx z`!z2FxGc6O|JIzm|ehIQt6bae#-%w9IB zL!#BEu%_6(@!0TvWf7|w)>to($I0ujuiwtXI;HAh$8-bid&eWqXaH1^DGPIGI9Vc$ zAR$mH;@z<3H4zH7lj|g5?XIE-Q|0-%G!|D`Tg?ScgX$WFqL3{KL!#y6OXO6va!z~V z3T5PdF|KbjYh})4_1D+0r(q3wGqCQqU!XL?y#rFi=+xfHz=Gp4MWt4Nt}qh}wEaZd&4V;Dp5BkO!bU$W9R3ma09s2Dk`_6*CaL-mjb zG535)&vEqF_!mu$`>4+vv|$sq+~~cq#u0e<{ADEAaPeD0y*?XXU&re>3+e*XJ3!rf zx&igQ!%_3-Y?x>)Iw@Qmj6~>`C#0R!=B(~2KBdG=B)bdM-;`2y zHU_4B(fn*wGYsN@_B3e85V;t31@>B(Gr{h!{WP#q@Fm!JuMQsZ-r1;`^g^dL$-b2K z88eZ{ol!4tkx#T$f2yAMCFa=K%)#d2SlTbFV?!QNUAuscg@>qg4b{&^wZa>v~K5NJ#%9veKzV1tnVF-Qu%~ z_9C@9v2HvX3$+Uy*qIUA19leFM7sFNSm@h~0qUp04efZG4oJN|7yGNdw$p&_RXErE zwv&zwdH~5@Qw^$zNLbU=dk4_+9?HIX7jy*BQRgDSopBlM^t!qI zMc!qad={cZU2!6C%db5#qnTKzwR^}|3vwo<>#LoGwUy#vhkgU>#t}F59U^~ zF6q?+NcNg4dSJEi)Z%`npcP4`s|*X+R&}$1jJxWntVM1jMSoexqkmo=jg3!>TrROE zoW#ba$f!LYE3nr`qfM~uSztFb{Q!2gQukmVAB{y?1lfLTcw?y0hY@F*QA@PS{h464 zgKb$I$E0pSjvlTX!*#ayj@cZLu5oi#R1lGLVcvN~ZNs6E<;p)ef)JVctk7PIawgX0 zwVZ}F2t%|d7j|gh5#8B)&x^thtnCig z4TXHJ!c`dIj0UjwFj7@kjXn|9QvQ`;!_kk|M)z}X#!_F7?aCaW^O>@kG( zY;B7o(ov^li%LUs;^~;&EzYq{U8f^6n9eEb;wB4_X5uf(WFeSW`aEDmUD&z*Ai{%t z3HDl!Gr^AMfjvrA%9m$;w+Fl213M@cu}$n06tJ_05EItk1Zz<-qpD1cDbpiHTi}M=C&)0Ao*y!aE>`?Y#*Lx?U=X}i+LTXUXmpp_t z6;z}5z_yG}?Z&KD0V{w!?|6?%F}{PkqK#f$vXFIWlwu)dPp|3R2RY#k-r9;9nb<6m zUZ0C${1`vaLOLeZLXw3WK;JtS3$+7<>&|2vs|p{wQ(S{asVYqoq5X0-U-x{EdyVIp zPTm5mjMb5TI1;%w8pf5XYAU>f@%MrvNMemI3Fi4njb0=$GFe~E*D{>(bHe=NG?*bU zC(g0$8ZyJ~9g2*!smSk*w$qB!$)iVCB{ljqm?U|fdtT`$Oqkt;nH^xx-oC+X9FZ=* zo-hrbh+Zb#K64w9#xlvbnYbd#%EmwEjB2 zo(8r>#p=-ayOKt`cPPr-s%oMX^}MFUnzBf&zRe2O4z?+;9z_{Qk@-+`qc|3&DrOU& z-X;<%jpB-X#B0sGJ`JrtiM3i^m7DHZyav|moB3LUGg>l~!mJkkn*Kx+n!i`0N~ z)dH&!lz*@mIKFJ>V&S_VEwr%5>B z?5y^kY{7HABJZIXJ=e=zHb3e+oH-et-C{v7Cn{D)^aD|Td@wATD8rc&O0@Nbe66~f zD9^{@43vjyn_V55-oSg156(l5vGiw*f*Umdq?cAC-i;z`hOJoma3TlV1!0-U|tMrgF;Mn04+F__NsczaG032KJuD=Xrxg?qoagQuw3b<@ey_6?pA& z@bUq8YjMko;1w7_%`Dpr{4D!EF*tIYA$XLLz+DYbZQ4Jz`RJ)Fzw4=8i&f48FBd4# zX1Z)x-DW#O%=umQJIu2`_t>tOXYax5cfnh0PtF5xUO`c(zzL24;hi30epl%s=JgBa zjX%%4@-gP+PdG0kHFLF1@aUkB_uxHtV6;0i?}B;pqd~9Tjd{6$26(tm1H6Rkf*ar+ z=|e8TTVyI?UL3!u7e5^I+Fjw7pVbepmMn413SK@}7D9 zKJ)UDc^^D&>n9u-xuc;uCKp<}ovKv(EvZWWQwPQ)=zYeC!AYALdkS1x5cZO*o+}KZ%$$wnu${iVYRT2K6!*_^lNY=M`%nz_0v=!O#9vRBK{~ z`o{8%rrdY1KgX^Ap0{LQKpPJh(At9qw6zxEL`vVq???;r?+m~4pMugU7pN9yUSH7< zAN#!iNHYOTFX==oExK^l(XiV5ctM_jwVsY^#j+ficKY{?C&Atw;r4J$NP5m?psp+dPYBhpZ$lyum9(W z(Y;AOSYg7G%kJ2WhuVsH=4Kp$9AP&Z=SON5H{7bw1nPp0u}nLgp)Bl=Xn zi6d(FShsm@wr!j5KR?mO`Pnpswt7cm_=LOR1MrpqDDeF!xf(WYfvEv%({bYs>4$F! z)WJpbc^H?W2(5LTf$)OD_8`^SX66TnmxMP&wtl&``G(*$EZ8GFfse3DLyYP3k6?b$ zpSuc2*pI;P@8AuIpK&p~hu)zK;@=f||4FWe9cE89Gc5Q5rMiDZ?wy$J{fSxrpi*eu z$N|g8YELnK#;x!kd@1}#fiL~%xD>{UwZ)ll$Ovg4VGbo5Z`W`7J7d8*W$;BP@*a{| zzQF2(tO>z#$-2UjbR4W_ALXN(7VK=c|eTGu*A8&~tq* zWB#fK&P^M1icbdF`}4e3)%pbQ9UP4POT};p9P7$+gKtZIDlj;EwgzoIY!m0NF{k;| z?8`i2GbQ&o_44!kUtSc`YyLLQu^TCsvLemA9--b!xP6Ix| zJb4F0%n2nYRG+Ny?Ez5p{eih+FMt`VIE^zPfH_q17>-DQdEV~vsVB-9L3wJOdm@2y zT$6b%I+-Yc9q}xbAr)DJeCWVcI5@2>28|fQD;HS>h@C05>71|`og2>yaRJ1Tm>Y(l z2=Tlm0%G{O)J&ft!+iS$9P2kH{DEt&%HX7`@sR<XJ~+7q&+v(o#!ho_QKFGH?xh#g+PQk;_)7hD9>2JwO|n_W0zyilqDH_ z$^delr(+9zI|_YJ2r^8CX+Z;!;R(hDQM|7uY^>0Q2bI&w}|lbvrf{DcBg`esNZrtYLJybDlPa zNZ$*3HdFD26ghtJkkQI8aS%Il-{}%#ONSVr<8U!S!XBRkjE@h$&Hy2sr>ul{MqA$#KQS(h7cc}FmGjx8 zas`#)jcu52o>Wc@s59H`6ZMHQ?m;%VjH<$0h+!G)tmYT23>XcIezfG&5?xw)rq3u) zUK)+hpT`bSemx82TFOBlXUB)%keMN@EG!Y_nO6;Z?*U}ZVt;9q*P~#Okx-4)1}jpP7q}X_!=%R5h!Ped0t$KJm*aQlhFEQ9)4}PDsv{~M~B}` zc+P?u^G$1*+u`9?af9}u|H%B<@Sbr8?tD23NZY^pH3o1-Bp}WaIO9HJkqZ|M>onps zwamnMem}q&jusPVqC5i!P{s)gl;_0*-cpDykvU(7Jx?OaZ5@_dIYjxK>3@9NIK$T{ zAD(^{7N?mr=$$_5GbJO+qhz9ti~v5^Gb0$--PMTlyoNAqc2ys0N;`xIdq;dW5?5l|S6fXc)^C_FHO2C->Z2jbS|YK z%%Lq|9^nw?lS*>HpdDm#6OY0kV7rf%X$dH!uJjA$8HZ8Rj0GsC!UQ7>hUGHE zUKW|>Jx;_P06DVgY4g!3NqsE1LggoHDPv`EBEG&}uiZq_m>`FlaAJ&YXqm+~fM9+nnP|02cP5ZIl>4(@Wcej%XM}0$4Lhumv&B}2+3|N`2gf{2jn4)QOCh0q+y>{ z<)-Gr8^$v)!GlcmD~UCLj1BS#6qALhxy{5GK?J}ees`u6xS$jjhvvUy$)q-djO$5; zgXc5ep$Y4d-7P>~`s~dg#|}aMaTdsjNn8rYM_*$jL=IR@_vuKKv6*M0%tc`c5E(JE z`h{c?yA^H##IruLt_sUg1IoN!CoDPTB_t(Q=NfamNG{{;0L&B0A}^=;uVFntFmuWS z0Wzl*%xh81ggLkGXTfa9c9$^6waoPaVG6yic1S<(QrO?x(2q-9)&n!TXCN0?!~KV-WMn3qJ@=FtwCekmRweuLd7zzm;sT=fi9^oD&?fEgJ8N;(~BR`IgueB39@32+4(uU9Om zDS+8P?TFlh)uBLY_s-L=%==0)TJKGm%RC4$Nz)lL(oFJ3`Jzg}{A6tn^ZNMfIkj_& zMU_>Abt0G2;qeDHvbTzRh;Gz56Xrf+$SB zvG6|0pur1v;z~!OV=HhDg%|8H)9%8m_A{OaT!SjjXMyuNmrHAN65>40!nu}o0Q2Di z$Xgpx4k%O%*bhEq8(3ozF3$v^V248o$k}ftwF6{>G$}CULvfQB<9Z#r-vs64&a5s5 zmW^;gbLyw;Zj*&b6#vrfY zG#*$P!cOL*5>Gd=daz#E(4|v)FbFY<9Z2pb11rQ_tXlAT9aXd+u=fPWHdw#)4GIw8#GREoe9Y_(p(e5`PpFc0oQ!>w)r=7a}!Ks~|J2q5Qq zE_PaZ3*-WjaS{S#7By#f)__$gu zqnQIBzfHK%py|i!iL+L!mNL*7s98FJOx8+&k&PKM2WGgLfm)@(U|jk-7$1mSfN?ns z<6+|>OnKfNa#dI+`vX-oIq8q7rG+@P)Q}3T zhGY%uEuc&lP2*J%m5elia;DS~TNZibtxb_O>kjcWv>0@5h3X8@lZnmOJbzqaJ!E+=6WNe0}6496uC+5dF zL>4|Pxg5GKn}a_Jla3|I2d7^8(c)Ps_jJ(cOUE*o`O|Z;-lQv;jC3|(103sObz;tK z%1 zU}F^ySMepp>yytW#OIVcp$U!$a;;?H4#WdzKi1iImfa?muy=Jn&0gT;X6#G}ii_eV zk+F@B2VXMEX2;Ae8rt0ixpf8$T&N+pXp}5RMvHks5#!1>32K`YW25dd(>*+9=87wj z4>T?lWczs*$a%Db^3buy#fBCN#Xzahz@o?ml-o=VIy=rlIn6)okrA|T7c^kT4k1Y1 zkQNtS{955f4JE6_1f$O90WEdLWNd~nh4oUvoDF}38l0MgXqK1GlMm~)mnho-%HQq# zStwgSP-~@QjSIl?aFP95J1L5N3@W>!q>sLG2FNh>C&>21GA0i`>MDZgc(v? zgVs3%=EmL#@7IV{Gh$TU=OKHHlDOouzEU4ZT8g-;kfXn+{S2`tJfoEJ$!7|#HTVoxqC zP$tQSP}eNFS##Ujb3@&<^ZKllFQ}~-*nNtr17qmB=8q6LBE(MykWuhkqP#x&N_w@E z>YQjeA9eYT1unQjXa~DR3JcM+NJzOVYHnEDiw!x{%$n(9J@LUs8NNqUzyM@)=Kyjb z$e5PE-Mg@MtTqT%Fqh0VXaE^82Cdd-W`NMvfYvM!&9J+~_(0tPjN4fl=W(FbO2_IJ zH02^!VHG6Gm?@YT4;qnnSvElEQ#0q3qPo?SyU2W1KZE!f{pmil3(YWI1n-e<9-NmISb%n2O_O}EN!7bk5)24y#hB=d(J4&6JdCbVCrfc^8amFGj`Vw7gX3 z18Ga1AfHr&LlTh20m#RvUT01)K@O3cLJ&8jR8dfLL(s*BGUIe^>AF-CWMt21X3X;n zXw7^9-M&H|8zV&xyx%3)vYnBTiW1W?g?{4 znI4jq33FnNtY-?nIf|X7ebYr`KHYreJKIKs!4-9kmNgrq9!E>MRnvQb4ZSF_EQT$O z%lL3yPFyiG1&tWS-Heq^+)|b>ug^VPyG|;@O+JN#{$4&-w(zQ)0dr*qxy>wgqCC(H zfoz%QUTwxyPgycloY1rpSmJvuY3WhhqcIZE0q_jL8J&2J*3sMrY@|{s`!ytH z0F0V2G!~KJVK7#(K#ZlzOe}bt#=llE2zQdmR2m>>Ns3x|=70%6hQ9_=3%ClbKt9m3 zq-6ESSs+_KkZ0v%LCZuEZPORj8yl6Y^9G$Yafa$~<+0Z~TmMbD#SAJFrb#z8Kodbm ztD`BDV!XF8xtL)ZC@0l5BPv6zG7;sdVYC^$A~pb-NYCr;f!KU=eTp=sRwNk4l%V*XQ-xEiveSd?XkVYiwG{%g^3Dzor!9Nt{&M$ z!K)yMWsZTr2*C)ZRj~3J2;&;KM7SIx{Piq^=f(CQe>*mC>6N`ea-z;zAzzDyA8vi0+!g2a1+?YpSOd zH!BQRETW7&omwCxAC*T-ovS;KnJQc!h)NJ2O0id{&zI0(+r?cvT4wI(5nn43yF)TF(ofgvK7$|Jyp&! zyTWh;i&qiK&>Kh{%S~XOIr1~cuK4SMH8h4$MM*?sxQkTg!4)Mv1b8Y?6zNK6rvf}(+<~#W9j@8@0{@4Hh zFW*1Q>kScs9z%!-Gk@5>Pj)9Dl-7Y3whBaz<0B*#g@if}CxiqHhan{7rkKzf6mVE2 zIXfXJ)OiI==r}*Y2^gE63~G2@xsg2#1chKm_qbASzb~I)IYtc8asY9hm!SdkJr zyztBwa|E###Z_dZ6GTe_5i)t)mjN*K}3nSRj zMqvK!yjJyO`vgNVV3=?ZO{} zVgSJkj-ydU#1srfjEpyE+QU`}pES@Ca{jHP3qke%S)sl`-Pk2i6P|l=)h%R7-l$5$nP1=;l`Fo#EN_% zukw!zO=~a>2KE7=D7+7coTO0fsXP zh=<+& z2Jx`RMk1!}M6|Ld;wLzH0mOpT5Fpyf`+(TP9T1V;07SH&0;ORuykJIxh+}(#hz?|`89x=5V%Z-fJr9xEYBSbc~jYi0Yg-~1Vd=D2tPba>$RQ9Xf*8c($r}Kn7lP<> z?g(OrQ3w_+ZV(-64^uM(am?H!jP3_0uZUn1fIjR~X3;ILkjgswgJVZzQz3&xUzq>` zPf)*lrp!uZSb`2}0EoFpEV@hp!E=K-1-ETtG+f}q#SPQguuA+&o#XcdTvmyD34aOZ zg!XDe(bYA6pjrif7OY_UK*3n3Fn#Y> zara6zVvhC>{$UewMLzUmqW=M!Dj?VxPocut53e1B-qsL;U(`_)0X1CSqo^Q0pdm#I zFpUM6sPWkMk}@*ZfLJpDh~^beBf$?OGdXh?OwKWN(L{{^W5r|met>|A@S!ak@YU|6 z?%~U8_|OL9cuk-gOC=9aqGTdia@e1N>s|zQ(cTYc2XY|cZ}vO^G0_76&GQzB&{xWw zaqnK=NRERPBIw9tdeM*#CB_oqOSD^DD%DfH;cyfxa5}<>J~Nzjb$AKb-tM z&MaIfAVb5k?|A~2DEwR&euyfNE5w1=@#}w-02}#0oR|F(TbPeY_(kwJBZnjW=3D~N zet;7S`1vq)aO#)j9>3)sBBogkJ+(XBm-0I>bo#(VKu64UAbKJVm0K)FG!gdW5~IYd zLwHi(<1ueh9On0U9AvtbEsYg8=OK#W2k4%Np!&DF@oU$@zd(wE4s{z_wov|1_0sw2cL+yQ((E>|yP_F@iKD|lpKCt~y1%9x8ARU`k zg7f#x+rMq$ppeD_uMn5?)^0xF8PgANCJ{gQTfooxw$CJ-Fh~qH8bnAH(`x;%M4x&F zfEENG7*6^MWHuU^!J_67q`HtdRN2_EfdZU8V*z?CKyb^T!~$MI064!w(hm?K0l#Dc zg+^w3_h;a`)Sj3kb`l^#G)(+35DUkbVTd2uW)5LKkiCF44kWSBIO0_DKq3+A(QlcU zIGn_!O6gdC^Dfeu>JQ+4va2ScbuqEtd*rU?#;f>wBPHLS z%82;~h>8ec8#_41vOfXW<#qINLPynsf&uNbL=ZA84mCn?Ji(+zb^)ZVKqeP9{IHUvtOA)(`h)oI9KIhQ76N<~1xMi5gZzC8 zVO>*?>>%j)L9PPpiuj?S_yG}}@bgaiMebf^xElEjIa|=Q6nPe=x4sTG%b_9wZ!nQT zneffWbYuAe)*#}C5f$JUa@+gQ>vDReMx$pVLD3%_G&J;}A^QiY9gOuCm_kU#FE)(6 zWD34Lu-6UvS>50(f-krP_!JJK)IKf5#rgw$K!6W-CeX8TkUm(~(ZPA*(bwxMQVFb; znc|D8C6m>-1@ydjuR@a^{vz_xPLABkt!&AckOUs&upmPn26%Hx(pZ0h=7-=xlnvC2%X%}yKF)wXw=*F|(xFK}^yaD$Uo=&NqLa$68~gbs2LgpPTX zZZ2S#0+XufgY)37+@SMXcbI^I*mN1O%p&X3et?pP(A9)HmlQ<%GjClz57c|%d3KjM zF9qztCW&$*h^N4tO{WC?zJxEavm@h{Kw~jq>UD#-wB=kNV!}>_2%-7f1YezVpvCqB zR6BsrN6UiG?GpxRT{%ojS|O{Q!>+(WB;iMG4%cJ$iACo?-Br+BR9GZbyMtEU2iBi`hMZ zmzeQOh2d9g0MFDbjYBU}&tYJfm`DJva>p)(f!%zzI=LTU#{qVh#>Opv-*lDMHFMC_ zpi>##kUeBUbOpi-UZT+ce!z@XW7A7iM}8Vz(o#{VxO#OF`(R zX`&$cAK<+Kdch;l3vxEwFT8cN+=i!IJ;z0`FF{lvDvTSfdD=+z*v zjLwHQPY*OrK##Tc_&5>}bxsqT{14FC5Io2>1H69l?+?9ovD}m`;c+A(WWdL*DSlVL zm)M-E$cu7UBIMS@oafyP$`)L}wuNaPwQ*1apiCEq-JDfE`5$1a0d^5nS-`IQr2Ca; zZjD`l+rcpea2gXjwDZ7yGeH;GorZEQpv#Q8GKgA@A`?iZLNGkBm2V)Ig51-qlX{@#5_e6 zaL?+SiDD)tv`4Gn3LP9I+9K9bPmjQYSCo<4GnBz+83b{B~jnl`OZ2$)SW>a%$ zBzk@nV>P4XFTHj(Deom7QO#MNt zdEi&`9KRnRg8_ayqGPt>cQn0~*5&bj7vP)Ori&1i@Fg|IBL)F{P`l?SC><7XRMH2j zl)?2ZJ?6|2(6;Sehm9W9aP;u*{KS)FGp`Zej;1;xb^Dv6u?jubf6S3pX_hrnimj zqLvFrcfSwoW0mQ^AXn?qS244CB!WDv7gk5g?zUGu{ja_OSp7ITq zHO^8d5=8^Ms!eMG&qSq=;i7<3qF{nuBS?vJ?hw`+;5qHuf)mlkSVCF}9H;aHTq*#s zfscpi9Smlrburx04S~;lClWS*msQ|N!p>D;ru<)*K-{zc`xFD%Dds ztBE|maSWK?`T3ETet;r{;8iq+LBaZ9CM&H=;jVki{jveG9BPtPOV2kI$UGyB0tP|i zm$h*lA&K|k6C-+sE6SMVgkxZzw5#``6#*|1DEeMEHJC229W_DrtYCugmP{e?jID ztWy}C1C21*yNq)@XdWqdsNLB;TKwnuMVrai!g_oqADO937^Im%h!_j8XL9B*_?Z)K8z=YFdH+TeB5JX9{TJ(Uy2H8Iaax{hh6T#8h!*srdI{ zsQ|W8%0Gga?I^vn)@Q&~;L-o1bFbjsL&e6ZmM(H{Hj*{+q8DcOsEe2ANAW#s=Nh$7Ky)Ibk~A3JzG7G|n{CuXX^0Yaz|;mn>(Cao17j?Glw z&6Ia$vYDC6m(5f^m}z08Y3IV<+NtNj#ol~3QaWsAO6ztA#zXAJM!TG24KJV?>RGhM zWW3$Mg@SKU!X2u7@g9yY(@T($cw`eqB`rB77@;-FQC+A)*!!#cxr&4c&%9jj^n=Mx zl2(TLyKyDR#$3ChP}<-3t)w-30e4gR)^7UWdFU1HrutzwwX@yS{%(8-B!;0k3c~m! zpS+#<5aY;0VB)B^_I5Wx8-;c<3Sdz-?@r=EXo<@o&$B=6Pd)oT<#K9Y^C5mcW&iu% zw57m`NpBD0KPBx0&viq|_q@F{@%G}Cj(PiI-vr~D5e>^c6CT?+qn z`}gCY_nyVrW3f~=gTwU2BhQ1Ge;rIW|NUdog+A!kmh9W%5Sge%#&)@wIAAhxXv)vSxZw>eV``17IyZ;;D|Mjo`?|=RE^5@ro`Ah#x z^Z)%Y!!rZcJ^=gt$DjYx1AgrP@DF@%JpWJnE#3~T?b~j^pRk!0bh&hBD~d^JcSGeh zxi-j)FuIc|5&Vwqr>Dck9=>*WwAb|0M59MG33<2JLp@mRF&-}Vn5T;!yj#c-1#s^) z4%WfZGcpP$)#<%)iViIj7&pTsm5#_m3Z^bxE1a0;Kn%*blCbR82FVZ;ZXXv!S%_BK zto{@fyrn-}yvAP&-y@ZqBkQ9bQ~ciQkM(=2KkboDM@Z*mr1KQ%giTH%ar76nRfjDk z7-nwhi-!l4K+CJ?Tktn7qj91>o7q5ug81O8uP)ZKnIZp zAu!p1J<`-7PssxuAb)JGRd8(j;|zEdS4I?nTFiE*?|INSP2g+ags*-RzP5+29l%%I zA$-Lx;LDOm0}X!7Km)B9>PL=h)I%-u5x{h$BV}hWV)4gY5g4kf4`k`U`M}cyi?X}; zRV5@e_I-T4u}6@uQU7v%4pB6kb=tqT6J=^Zx~6VJepBM--xDgJww zDeilgDSl^};*Tt2$Ck0VWuyt;EtAPmKq?L>HL|lFyqpPsVCkD0#ZT~s6ZE%L4ar?F zcO-So7{?&k(`9jd{`T5Tc~?i;j(|hSkNTC2`26;D?tV0;>n-uc%4Ui2%KFh}ddu-u_#^)9 zfv=qy;G0)u`Q{Z__E%&%x+2T*6!2(8D^!r9lFm3kqYd3FKyxvV1uDR zB8(sucAgDV8B=}euhPT&>y2$KT+vmGyP7BUeDlZK(!mFm>o@Pl`pvts?(fEWbT`)H zyRk0rM$G2*6U7-@Cg^oUv{WE9|19?bouTLeWut`%l;G6qMH4{2d6m^~US;*;RW=Q? zYjn3^cD=psj>Bvs;x)~o`ZhEA; z$YE2&(HO(RDXQE1^MBt`pn7Bql-W@H>-g{U`#O%7Yi~5?zOXl%i?@5DIXmAQErWDzG%{qjJItG_U)~!W zB^7|_=HX~}H1Ga~cSrk}_uu|0?srEsceFd2**r~xW`)*w2L-v`$^p-tdf0nO;GY@X zadkolcAWZNYnz=0iR&43Q+VNc-B{Wmzjd;(+s%~E`ND1%zqhc1ePv<4k6LWs7Qf)7 z6m06gu0bteyEDH<_vLl{hF$pZ>EW7=f6Fzm7I!tT77uD(EgjapTAJ6qpa=nD1W;jk z@@#r^U?BVP{I(p?6i*cdGQQ#^4|>gHb>{Dts#^M9sjB6#l&V_(RH|zE3#BRoQu9FM z;Mgtpjbrx%njaqym+0S4r{s}I_L+-zl(}ffnTxi{Tx5zwy_Cyk9TXzy4cWq5Zf29OMP(_d#C&$D<&x|8t$!QZXL`f#*bGFLt1^jqUvi zMQWXrWGH{?1?0m%T%Ez$w_>{f`ftT_{q^-Lt25ljbp0RP7i#?gBjka`!SW1eG2P(8 z!36#XDBnLlT$|ymjYri3ptvhOX??7p%@bNsYKvwh(J1tzs_ptRi*&1)HA(%5^l z6@SDt-##5K)ABbQTlL@GcFVNHgJoLX;WDk$*eW?_*x#o}eQk7=1on|yDDQWCPV5o^ zwfKWx@t~JXR%mnITcI_6Z-qAd$_j1y(+X|pF%scG!;}Eokv|2$?ml#E=zR2rNY)L> zj}J$y)Bi1Z2KL|nbClHoX&)s8f7lt=|2fWAXXHq63Yq*lg{5|6P<(w~o`#l*Ch;k~ z;z2JtUY`DY{e1oR`&N+D-~S6iQh(KLkktPne_?q><)#G4PMtjXvwpt9J|!HWAFymU z4~Gl%%iVnTZ?BuL|Mq#eKo7bYfI&AkxJ`3zAw;}7{)8Pch6RaZGkp64L{@| zc&JxA@FkN4`n_(0{`=MMEztCp5XoovX@RCMgh<3A+5lu1#rJoOUtOR#SblssT%bAs zEuWxy7b5v`5F+_<7$W&H50M}>ibkdeHmS-U(kjA~_b7gA97ColOYa6RdeEyTi?n_( zQu6hCk&>@piIjZ%6e;=kg-D5*U~WgC?AB<@XVu#JUakEA=grgMDs6wm^{-%V7c7}O z2$swp221AW!4fEN%-<*K4cDzpDHJq-Qs+wCQjyIT!0-m@Tl$OZ~Jexra8h2%wq3klQf8=l{++|S85qhqSu%0c9?qo__;EW ziSVPh8vhJqpf%ehTM9Z%yyOj0y~+FMf80@lyPsXPzPDFlC*e4Yv|LRX_rwXN{_Y+m zP%9>oN8MK{-seCP`u?M!rwk{{N&dZ?@V<~9?E#lsL8^~)B&2L7Vb~qNF4~-%&;M~c z?bh*~(lhlS`nUVvNes6A-(Oz;>tFuet?~c)*I)ki&;RwmgU(2et`?){OFukPt{*>r z`0xq{)1p&5R&-fzdl!EMen9^t;0pZZKmVs+|DwO;zy1&Zur22Q?fp}}9xv+OupL6% zU;H2c|M2I3`CtBX_c2}{^b7o_*XKX~?qB%J zpa0{(yua+>|J8rRfA|mo-~Z?TgMWHA3n2Rs|L))YyQOiWE6IUVm^EW_(aOh9A3nUI zAuZ}c_tVH<>OUKYnab}N=dC)(@getpwAC07U82ux%~7kz8_Aqvb6_n*=Um8?Y#X0EeE6zI4HL{k;>nSgQ~A5vG_T(@Osh3_7Y0X~PtG;C*&jZA`0$GE?{pU$)BZJe zAIWTIo$q=BrLK>(P>cJ!x^Vu-?^>s|ZV6p24mrm`r10_cx7QdY_u72|8Da~&8>fEP zAE+n(Kr6nK|0~}(trky>SO<1-*W=6G=i5tq;EORmY#tDzY-zO**7>eKQ1jY4)+6%v zf8{%;j;q$lG1lPby7w%KvNY&L{G}^xF9JIA_;QB#Y(%+MP{+9#$d`Pf{uMBnc zSA9ybhOZ2Dq((hUkcnbA`%^A3tYxWGtR0*b;m)k@U`#U>BS{`2VdSYx2DjK9+)hyXWr>pl(WXYU?*T;MI-=cp1~BFoEt_exQ1Vw$cLs^_xL zvGlmxB2#-e4}aVl>8cQ@bGC=+LI@fsah-^b05WzXC5bi{G0s%uOho@ug190EyO`sI zc#mTnHU26{Cuh6ER5Q3KN~NqNq{;BrhF@O-7YX#`URs#}KP;1EKZpL;f$t)S*alL| zf4U3Od3^BT?@nYw=)Z6U0RpMxyWQ@Y6Qcn^g$HyR1@eKyjSfzSXfa>Hib)1t1j42b;ieL(s$cXX)Kb&ta4mMsQ# zLN`RUbeBL};i&-Y3S^cnq1tnw1Nnxu2#}4t8$9SJx~w6OKLI%g+`$!5&^hhn9`khp zcXVa=Cbe5kc5@eQ)G1)(KrsYjc6l2zI{x$PU;odyhEWI5+F>5O?0eM0TCIy~Ld6|} z;2iCqv4R|u{@}vUAq19^ik`|4!R%Q%hRoswi%s2!yn`xNV!vwc-+*ce&cR1?ypcCa zBJ_9G9s@K9JbH=yF_?JtfxQ50y|L2(N#@{|sSD%z)Mi+mS5=a+IJw1k^*e zg515WsdL1+NVcqdoc5)p+XtulR0v+c%_S9?euKt~M>aG+Y`8XIUYgLZGNC!Jo4|z8 z?OlkqPT4SFs<5E<>5f;Q!GaiR^wd9zlq&O5(?gp9_8@!-FG46MQ6$WRb(1BIJM~K) zj6{+EL18geo$K%l`kVAk8X@s$_oC=Z&=L^~TD;5B|03~FD|b*^lEs?8eHf#b&}O0m zv*^4a{sIFglK~?VYScnZOA9Wf(td@f<}b5g0mUvXSa$W`5bP@k?2exm{(-Qd(SpSa zEr@&E)0zQ3U$k5=2T8 zw_ta6u>}KyN=x*fDvQDJrC>Em**$c(vswQ1Boi7IN+z8!344YKOOpvHyh9U)wF%?W zgzhR624DsgHgA4!!~6vs#*T@JTp4o%eV+u>wfYqUZe-Yy_M|jc!#QOUxBSvq!3|l_~cSej~abKd3Bo@?0 z3=RSMV}jrjZ=`NSUui^MhW+TK5j}O2pg28r;kYaNf!eeYaTfOjGRU=X5j>l&$%w8m z1SoSdaLE)9>PAG+1|w1(r;%IM$a&RYf!*D$2C22%C)qF-vJruAm|9s;HViT$9d+KT zb!o%6a%g_l*UM}O$y3^}+TH_?UvX#xuEPwWXxPwVh7F@bGfb;=XlSsM`l5m$q(cj2pm0?zZmkJk#w;}F0FsoBO&qIP4vkH}5 zx7h7{3vy@Vy8!3axchJpf*DgBQ-xBx!DKEAw+}L1##YUs2j}ScX1Y3dpZDV$JUKy= zTbl0qD$`|<&C_%#>|L4k6<5YO+#n$J9H#SDVLIJ{6`qW1w4E^o><2b&r_F~5M4T~! zt+6I%U4(Z5yKij18qqD5MKKJcLR67i9wez?zrs)wtqL$7d*dV*n8`f-5D+hYev$!8 zq8~$PWSsd}2x8Kw0ed(ppQx>m&lHyi{B@ZDYrkY@z*^hhfM4-rJR*2NLj$@@1G>I~ zaWLZebUZPG@d|X5=O~fUL}>VmC$%nw2XSIH1?UJ8OddtYv+sjD2sCs1rM@Bzw6aN; zWj{azB7dU|Yo;4x@tUYdzB5rr418$+vi~NSDY9h$c`L_1^X3cjrmkq5bV;V^+k?`7A%bDW1t1$qWa8! z^nQBdz+?`Z{sNN6fmp(~EZv$gU#v8rm#YIick3mTKTJv8N(E!MxiVLi5@JjB)02$X zBK?=>$R=Ed@lqSc^Klq&9nEK38n0euJnF~NdVAUI{#$*;e{pnQ%xm%k(v*#?p0FN_ z$LJ4oGDNNlO+nle0)-Ww+nP%VkW&b`haP{nq|Q{M$=nuYYVR2mr~1R7OYu! zpl>PcE%+7p1&Zh0gkC0uH_=U4kmN+bt)mgHIqyIQ{f|Z4&jRJEX~7FM$&T(08^tm`CpaZCH9tL)d9!bC=|rGUF&Yc z9JQfaS)ZY$E^QbtvmwaAVMFlj>}*)w7i?G&#dmK|dR7=!Y2b*U3F*V|q=cSTP`O+Z z^xc9faJa=_!Ae8xaDj8)W_s5qXN7cB(jE%U19Q zaSJS1lQy9zUq&rD`})m*3pB`R(SLrD36rBE(_Lc@vT-{*_*cV*Zd`#nW9kbwEC)I5TEAe!(%-@4RG>=HuOR~qFJr+-XH+KSbaLhHDo(r56{@VTKV%I3 z!GM`XUzQBb@STjpVL#r1Oh9|8;J|uC#>4Sib<=T!W5-buG9VD}pPt5)Ax+n~`=C)y z(?yf%VjOp1S?6JzTbM4E-`uRa?oi`v;l78}nCC-5_ufp(f#~xr_pC-xQUgvYsuBIDM#ByiO!_f; zM^+};KQjqhlP)du@LsvwkdG_358I3=eVCfC)SOq=z;coKLK#6|UZ0*VF3tCPmHB#@ zUS+=+_DSsa6&EIl1p7tVFBaMlX(%YDLLRrVjBAk>V8sUQcMV+_Pk#oP>4!x68b8+i zSO6VnRb44Hrh88(Mfv8`gylLwUK%hS899h7HTU{VKYzxG<@^iYUYl8+HUxt!umi17_7J zbO2Qz*s!!j8z$N?P@*$YrzbM4#-IYV2fR$2dV$FRDFt;ChD;Oo(^4kv8$6%{Z+t1z z*D#?h=&|i0V!J@!_tTRsSfv6*6X}RW?q~C1wq84w?H3k|&XyK@y3B&zv8M%--zT%9 z`-%_Kk55<-W7ViiWEij?i!>mHm_=k@3?oX$=ecMPDyVntkIanfz1mVzW%>q{fo_x# z(B8s*Rr|%3AOh8x^zM#NzpVml`Z6_2$=!ZIH&?$HkU=e#Cc}{S?UM}HaGe%*zK8-4 z_KQjO^TQc>OTEqPH}_v^hxgZ2_DfkYMCvZx-h5wiUt(qq;fMKPJ%-60;tK20Vm8dj zNFgf^qasB~lu0W|bqQ_1O7UE#$8^vA2@){<@Iw^XZ#`6fFhk!@y;zig%a2rz>Bckd zJ`gGtsJh-mr>;AtpPumR!^E^5d#obX1dLZk7RI|UjJG^KVLob4u_@Z*vOHGpzN)%>*b>RSHtBx`f}P-Y zVEZ{Jgk`xNbbmaypNral(K8d{Mb^+*J}jgTam>`+KFNX}>W_#laAKs*gl)tRZWN|k z3TqR_g$aGBmzl5)FSMULes9CCcrgLvb&+jCs7q9CK}Z?FhA?4-q0BJ^%&aTr%x((D zTy{;Ps|XiR9mNL&IY{?2Yy@^_>Ew(*G?igN+?p^Tin+#2?~@4rO<#qr8(j^^{syycNdPV`m5QqKgZLq8eQBi#VVx1n;7 z)%+V_KW;gLbQb*=@qkw`kzg02fY?4g$$9~4wZd4^F+oe~#Wt+x#`PDMdF;4o?JdET zbd~jLh{}WJwkN!c_7(R92>=w~(0HaB%;aUjdh}nIXz@)3G~R#^lEl9-kCqgz z{gQZn7F8EA`*B&6z^_?gJoXTF<0V;7hbPo<;W?SC$6O320T7h3$>QUatj8`!W)Rum zm}I?@Wj!S2Wxe&`x#;S>)OwA2Z^#MTK8qdgE8a`@*MPB+3duP-pC}bzdW`Oi+{(%b zFf$VazP)ifnTn`%9)e;oCPzkdOC)lM?+cM!)Os^R5g4!U5rpHY$QzO>=o~E!9nkxY zlCDP`7Tz!m6d#{tJrrSJy`=kE+0W#>tXJ{~{+5}uxM=8w@h&St^n+coU&($SIfSox zFBtqs{msad)FQ3t=!u}gjT$Fl^kwLm$VitF2eN(z=P#-VhkP>H&6w{*9O86^Au8$H zG*X5elVl~rfarjv0X=tLWd0gGz_3|ia?Mjzp;A5)HVf=A{P-jb=0N8q(y?+@#^F8o zUv4A#3+v!v(a>A$ce$tn(cLy=5^2J=PhiLJ73T$Y_=r3Jv)MhwMwyka9Ig&rAdx0y z@-wkGl!NvkdR<&&`XeJp#TGVIjoRB?l!jtBlc5DQCs>f8u^6-n+=GGnYeO;x7RLW1SIv;Q+aaOZyi4@dU;_#yUKnZWI%yc(Om{K#;>?8 zIDk=ovqt+FXC$h25Vkj}s%&#dx`(ewW%Eo3-uP!3wkR52uX~4|rKjU;W4t+xM zy(Ud{c}z&C-##+89c;8?Q&EBjr2SA!u4q`I&9Dq^pJc&m^k0&i0yYbl;lHGj4#yq7 zvf!eVx7cg(mst=CkQU74z=Ws%OJnakif^)D;RYt;tefIG0CnXqZq=otoIk)2t@#GAYI+*Q`I?mEzV-G|@D4(Cn0jZH&{&xZ9l z7ML~YP?5mKpdZ3G#(rqLM*C@&UX79dllPAkizLcNW*h@{1M2RsRB(c#OP?}UuwE`Y zK7GF&gPPy)eaLQCP$Ws|3jR#uj#iER_Q_G5+Ie~~S^3tW;2mF<{cPNSwvHYaoxI(C z`S(@!>(Q?6zkWgQ6ENqUycoY?@PIlyJH}wY#7ttXl;dWS#q;#+*F4Qf>jgATaP9y- zRBXWvUiH4f7Vf}E5ChKPE-FcaxfzPyr!<60LZ+B<)9wol5-4g7`%xf{8#=Snc=I6h zd94cmk}25edn>YD*f!rfcvy7tW_!smml<#PF0F)p>~P-1+f;iGOmc8mHgF~vX<;-m zP>ce$3(R~X1ScCVt%pPKF`!|haM+V0eX196Z)m$=Mh4y|!#4F#!=H(2Z*R0+DZ2Ss zIw~g51@1^yT^C1fxP2I=YxG6rKoJVsMv?10z|yNoASRQofto2Cgc@W_r^s$yEV zR_?vrc=Lsz*&y7W!H*7XoVhd^>ldCS;L2V#d`J59AOq4wNawIEXBe=!It*xI_xbhF zxoF`{-GWb7SujClofeE?A3EeOI4)=(L`8uHjD@qEzB2eO$Xr)WSED2f1F|G5rM7}* z+(%t}sj7c=REiwyhMs9`#not!P^Vwz)n{Y4L^Aa1z-GfCttZDt=V-+`Rg~j*4VFMN-5{GjO z@4igmh4-EJ4E@x!3$hNz)@-}h0p@8zGR==#pCd0kESS6F5P0e$Ps8I=H61J*g9UL0 z!(mN4Kb^8Kp?_N&ZcU_m&Fzy6h#7FC!-4m2Qz#RYyH6 zSjQ2H*p?>T=9XqAoI5ble&u?V3H!>|CFs*Z?z()zgW*K{a4b3c$FW>y6WN~+wtwWM z8iPM*;CMm?6LMra)q!7@h^y*-POW^bBvnQe+`mk8Cm6669ili>gO@8pz9AT}%7?KK z`fo^PBAgOAetMDtM^VujiH;0x^-RB@<~Q!Z^$cfRH1NWJ+8yEOt9MT434i<+{Q6N3hK zP8{hFP5nDTK2sI;_d^91EOKHH+qb?EWit!y(~~R+LVB36G8_%cfFiu13F~MFUdQi? z2Ht4>NV>{^6$u6y&}^^g73&wA7}nnb2W66wsYZCrS{j;a(XhlFC}|pFnix4Lrnhj2 zX6!()6>tZJ;bmB%S88md#{iE&aKE8Qo|vdQFIXueOHdDtD!X|(^#lthj?L#HM+Vx} z7+#W-gTvtVNfyLX!z$wVXo!m8F;OTBYO1NW^&z_G--QX|RVM5avF<@jc^^sCFZeMm zzXy?rK#7Bcxj}mn24u<%`qq$WVqa2%kiBQ5X9~Ab?iHuU80kyO$bbNmKyJU_;(-(* zz?PNX)PH3jqKR6LaBUL|$SOXP-+?SZS=S&s48bQ>M`pl2J;{P-6^8{G*u>GqQO6c6 zX|v!ul329yf?)BoB1Gdxfd$*%k!fFWWP~8 zMa6(yk341X6>`sJT58yvz1mQ8AgtrUWdC7@p(KCsbS)S(v6!JtC#@B8i zWI5XoZ|v=;6byDHEtk?*5OsO=rA6zmfo=FQ%OOiCUWd3h+!ve|-&ZoKrnDTgT&Rtc z0;UFDeQX1S+W|HXj>D%wIr7Bxb>q2No2M!gTL2TsFPV{1r>2XWQ;|?;zhn3&S$K3A z1~GxFz zH01FEUG#klJGn3TF0OFz^*8|V7^fk~y1^D`=Ds*<&iaG81(Pv=NW5HFLRY^bT??zD zI+4|yPi-s5c^J^Q`HgoX@fyUb9O))8A>-A*%ML&klX)IEnas#(`|(Kz1dH2)x*bg% zGF%CM7|^F-z-1C9E&6xOa(cbWfY7m%0rNhMo%|Pk7Y@OTGoZ2IhqZAvbD{iRL{MnF z_#DgcXtzg-xgjjaS1=rnEVhYC3roO@(*v%4Ae${Gbh!x)xwkR$$Jm;i(2v$NL0Nu=s3Pgfe0Y!rljjyprVvG82$mWx7zPy##@?0|oVzZ*Tkz#F3$n$}NblD6 z7Cd!b=-PAWE5NhEXy#U>A>Clu2nwm%*66r+Mh`d)ajJ5--74)zYHJ+A(w1v7U)4yp z@d8Acg0%UYvr_JO$|~+Ip5dR940p*zEjmV3N2nX!*qQ;%vYsiWz@fB{pUP+) zo2fkdks6W*%O$$a=-#vF8=_E;+)iftf<6*VikoAn4`YN*CBy!|v7rB2( zE>dw;>Y^hPwoXa(n58N&1r?@uX;7bTA7rO0M;H16k0W&Vab#EA;3%l;3C*28{-@Y%~yw?)mGEr6J2i8@{|H$AE{^BZmF?SzB7*KppHR6(S~W>`wQO_K{4qvalmhJ@Xeg`Gc!)<( zNES)k45E~K^-|uAH)c*4Z1w@ZZMr98f^CYgxaq;-9Qaa#w0#A6j7kLnP1S*F3`18U zc5q1WVFp1)u|^jnP**q<&ZgJEhT7;konA)>>)C)#dVvTwq`q+){)-=y$zj(24m2|i z*Zqz>x=9lcNQAM&u-?Z+ClqEtQ%gaG77_{Ur2%Uz@JXx;Ld-fAuAzFLL==h(FF}L@ zUD=ZK5Q#G$hnPY);*iqS-_-4PPyOGai!elh%cB@aE46*=z1Hf-Po>t2RZ2~c#$N^N ztDr!fj(6z~NGez+Yo@=CV5W-uN>W-kV&m9zo8SjlOB7u-w$Sa9PF>SK)H?}%^p`Yh zmz1F^%vcUU_unSeO~(XyqgM(R1!DrgPP@?({UGIidNQJM^<+e_F0Ps!>3-G*W{=K4kH7!bSP?C`$x!u7b7>wlGnUqdf2j zIR%xYF7_rpjf!gp@YOau>9H_ZjlLHRZ=$CUlliC;4TCir6m)aw;iWDXGMMD8FaF(X zZx4gs#m8;T3}!>U*0T}x&~EcK>Tb7`;#GFTm>ta){62@d?xKEB0>enRdPe{Tm$9t) z@lj@DNSiel)-93*NwZ3`p%0J*q2YtsATs7Twji@X9bD9Rqsr%0#I1Dl1IHX_HVB8q z5zR0gx>ZS5CQCU?q-|zuEW0J|}w2pB`no?8G155QqnLKX`H%({$we#BG#5m<}V_*tNrO84u`V zfgwBsI@kvF9&9(dNwiyr-Pj9L)C+@=*v1Q~>Bpqohe1mhRZ@zX=d&l%R)vPdq21<< z)4x-7@%1XZ^~D-xH(&Pm)w*#SNVG9)=V&O`bT^=T_0yy5#wi`IunQiE*J?)77wqOM zxph!Of47_O;X6zPFN~IJcmx*#PCjaDL(PTEf&nF=I zZHDj|cVhCxX6>F)lTy$HnskAC5rbLXyyTCk%}~-!`eFv>fa5J$*bn$-ueXQ6{oh7& znc@KDG?5`V-fHvq=-1cMJ-e&y6_FN(y?kx^3+|*nddvws-B*RP8>A|_eHg6|PU(=K z6H-@QH=&QT7h1V0H_f}nI0IeSutkfZpk5$|fwZPPc+O;TVA6>w99_7e1Hhs{z~20Q zA;FdIN=jq03v~5lOg;3suxXxa-VgoyTtc*~43^My9wo`O4*^d4p&!E#O-M9RPU4m- z`^V97WZGXrCnGs+U^CMm>v7dR`l9WF%@W$EnT!}VN8xuZ(JNf&fKgG4xm zmP_sd8@w9r^W*5(jM{A+r9@{~ep4Iz<4(#(8Zv*kT*%x27^kD7jD&}=U5@3(cIvwy4kBLM}!u!#T# zLW7?kx6xRk09uSN2%+ItCRz>ojKrb*&1&dnr7SYK#Ar1nFGYTf#y(oD$)Lz6%2Q0x zO~DZYD`oaIJ5r-kf=>(*v3S~XuaAX>=(#=%AOliWTIDghe;z(%7DH?qMc>HGFRk<{zmU{IUJnV845 zhI_4X380w|ks6Og#?fxejr5Zl_6pQ#j?TXs5*_Go5%$gFu$XDu&*_)bcrwNXkKO)x zC-cioPN7_7F%N2tNM6Iez4F>#F(@kqr6w#!Kc?K`?s4N@iNj!xQChP)=!YXshAk9H zzWr{r%(~-n@t`r{M>UUX0{2d{Tc9?oNqMVrtre=MK!t-un>ZU4T_Uu8AJpGT{OJ(F}^w=fS5XyjNc4K>C~W6GbNHqNS{ z*37wIj9$q1_e}ruZn_6GSomPIgzUdsb!fNBT4`jPD%EH-$bZK+IktnLUk?H|uob#0 zF?m-Ri8G^>Gzz^L?d@S0jU<{$(M(X4UKz9_tL1t7@=HI|L%hmrAq__)msIywi)*Wu zQPDt4WR@HjZ-P%x8^>SOR%^;SwBh!9y8VteZZstKar$lnTjlwPUTK!^kw0!a z?4q(&Z5RVob5uv9)vPgpqEiz6tZ!C(dl*(j1p&;>eS#Q=cCk8r&S)))KO4>cX@q%lMI&eEhjW5D@^!**X zPXc4OcjHkdp>4%BNP>}4ojo_mB@gLrv@>8Op%3Bx1dJTE#E`2`_gk4XvBvPW>E0g3 zrfVh{MmhhXsI;4oo&P-VQhu@YWUp5lE=Sh4F;*Dc-f($sI1(c=g+NQ?qNe%+=l}L` zlU0c0Y(>Lx9OAx>!;dhWSJ|c14~7E|6lnLipbewwjzoUw(8H0)n2+8AH!*w5Wjbo~ zVO3$cHQ|<1V?rd8%P`%C2mQ?MT}O&YD+UNdLbLw{$EKV5CeOcnEdF$r=}H*BNj#9- z&g%wcV~#;ROf(%i^pLaAJ@@f()1t|vcToqXcZ%H1anE(Ly?c}3SxNU)`PD&IVY0&H zFxq>VqGtvJ4QsmUWGb|HwZ^VkTBf`ksoaCS%Qd~apz2ElgheZ1p1J^rC0N%{8W7FlR<+ID!&^p=?S z^Wjg(Fg+JZ!4>^Gq3wWTc0WW`TR6~C1b*%j)p?=Z-JJXOls;BrLZfTRTT zO>r{W_(K~xPUGfxi5eW2+5x`VzSpXlj!j&5iiG?~U!L zXb**|5SjYe0iEtEwkCS;zg|Z(;E3uWAVp;#lL(R-Q7 zYka;6u@)ft!)}9p4_}TaaeO3T)XfG1dmaK8wL82Rh$OAMfO)syYc1}Y zDlGu>J+u*GeqS}p=`5q|aBZTwRzW1HYSg8Kj#Kg)!%;^qxZE(uokk-?O`jhJRR*fY zoQ}+VAG||8j@ag1cCR5nh3Bg@hpJ0|GAr$E3Gm z+9x!r#NC6PiNqak%udkPZKVv6zun{DLqe?-JX?3D_Tp`NxOxoOJdgS-hmbA<40BqB zY)B7n_kf+8|6m)UUGU3{wm0jyd+|NFVvgMSuquCr%YnD`^<5B4E6#pPo4@+P@(>nIXolu zpk!7)KMvkPLzyR3=rVyh1++lNM}PT&oM9rRL^R@6^L{2Kmy@I4GJ4`0X*oc~gnIgQ-!Xs^iVGb}tmsR@#Kn2H}pi7n>* z9b8Y)MvU)4LSE%DQ72#?79|y&EbA%uJa31(F0BxAR<^6(H=u3X$9r{={(D!FHrDPl z8R#N;brW44mH!e+ToMxx8mYuguzYRwk|IA0McX^llp_U$0i>IUf8>w(Z^yvpzy4l{ z1@mfsIYtNGtbG7;#=D4QRP|VQqx%Y%*6q{qeNf5C%-WGrA0(&ve$gUqt6y2Oj}K$i zmD+<4X}DubEi!e8+JVC!3yJ$Ga8fdi{My^YKT;fpgOXW!#6$es59GrlC|@u-7aGbs z`(cp0ZY~Psiqecb2D{N49L?0?VUlnHoe#RKR3u8H=K=md>VPp5a*P0rtt7E9-lT91 zeHZ`vkvpYwF14|6$A%5^t@wQG&bA*&odH`<-pEF^=l$KeZUhQsnM1x3qB-iguWS_o zKQ9=u3m98Gm2A-#1$;vq6G!B{$Bb1>OtO&!KnUXmB@?Bwz28Ou`EelJ&s{jqg~hun zMj|)yf7>d}mQ%V4Vf32|;h1*_mvzeyRi1$`bK8j6k+9HMIj-D77OTLqz=xpOw9Lzo zBd~!|ZH*8^r-uy z;}^j0@SG^~mX!{0_g$|77!7g6*XNygR{Y5&hbjC1S9;{pS;0tRGBn9-1)_^cIHFts zPJRDMB~VYbIKdl%C=Uw(j7fb`#tG5=;{dpN%xDG*E>+;bs$d# z!hP8sAbfJwamwfsP6Rspk%l%P48aZvVHL79q05SDsW>aEGJ*+{NNE2CzR+m_zHHlb z?8=kMrj+Sg{rtG$8?b=0ac5xi6SU3MALF~a=zRQr6~1Ocu0#(7cG<*YCl?*XH+W0w z2+UIO6SXN2_Cy%tG*M(h;N>bK>54T>S*nP+Q-*mwW|4y|Iu)QSM^HaKZ3t(qU}S|z z-6DjKODV=&yFh==z@6|=nhgbbiq@ZjA+)|hYebuzCnu@Ghg8JPw^PqyeRGcjxSe1o1m*i zXycxswyax0NH~A5;0wExDI}f_ASBG>)5x~Ly^=qd9xPbqb0K2^kQwb*>nV=u^Y!XSB z0zd{r{)3m4U3e32>33Mwc(ETMIc!35Jenh=0RS)>HXAi3$;jq8I^1&n{I~(!qnk>X zjidWbTF%F(L-+x{3n1LQ4hNUY0l+7nlT2wl%PjlPygM z`>-qhl;ehptte~avm#$HxF3->!x=%)i7>q~cn~Hhm6M2eA(C2XT6#9pUDNOFCheSRDedlu@N)-brSsKRl4NX%PEu4CMZpanKv zbS3SH_{&)b38<>nQmqDOdI>sTscJ&m3Pcr@{LMU|1Bye?jgr<{BmFK0?a3d2;{+`w zfsnGtFw+(M^td4$&>mH_h?`P9$3sMAL22~TZ}e~#zy>mHBJAA(!Y3^!(2_8)N#Ehm zM|%wfjl6WVaBkMj68|RYim#L*=}Kukl!1XSGIM&>a}zDCXos51awb;bC?WRGh^6Lu$>{`2DoF3(>km@yK@iY9{o5TVv7d= zpEXq`cxfQj!w>=vU>m1E;SCqb#|DBZw9hBbPYq4+DsF#7Cji(gm$3QqQk@8J!hk5N z8#j*|z(uq=0vOpIlJ6fPJnuTWj`j-(hrnzPx|;p2>!h4g+92~w2~K#@=%9o@%jrKs zY)H7L%7tkToxh-87F=Qy!lI36pWv9>(C>pVChWjz?o8qNPmcp)WbdV0hW_hJh`}*; z2=V-q%lS3DUIlUTBjwYBxdVt#ZaHX*Gp0l9UW152(=mZUCxQd2c{9fl(-0s7z7%;7 zfqe{P2$3zEl@vo%MAf0oxy6r98@7hnF>CoE%mcRm_<)$Vom`7;t|QlTG!s|Pm|{@u zXHOXBJsSGNs!j-y`v6-QOk}Vgs>pwni z_;M61vY93K!vo4T@$hu0^Olp>lwZQjRq)o7mFNk!?>VmSj9U(ChSH&h#OGm?dKr4K zC}HA6OsWDws4j*fcvpxoXG-l-mg8~^$&h5(n-QH|lx0k$VxucCXmi~+^yIG$W3+Lr;YL->a7QZiMyJMp|T zYfhJoGY0@SD#JrPmLdC1K}_LECB~b_4c~;Av1;+6(ht%2c6>U_yG>qwZsFHe_@+2= zJU;IAYwC#{BFPqvprW$`qklXcAWTDDS>)M^Q;P$NJRJFD#SQfSRDlXaI4H*xr0&c) zH~6xJS6V5%#|`0<*`iH^TcV1VKUTh|^KO$@m+N&17aKVq@AgVf^+dRka<`b4!#FqO ztpoRKWAy?Mv+KGbHN)H`{A47ISsmsYM#&urTMVLLH0f7oLK@X%!Xc*H$AK_f;1l^H zoiaQft7CZcUw@Dg@YXg$ICnr%RZqxA7%UhWIGBXIm+E@s^cQkoh46A@mGi)e^q8c? z%So`E1uv-dAF_)1m3|!)nI`6p{OM@}*N_&CWb1?+58ypkkf`&1lb7xQhl^^RKy@T| zj#!&(IJw*q&>9&D(PCX7n!*DDxf;Oh(b!(t%I-N=l*J!uPzE8s%sE6;M^9GCqmnNH zhz@@Ns&O|@8@@b%MStour6XJP$N2sLe<<)Z_4B|XO$Yd%TyWm=bm-v#m%}VVg$Kcp8F@YX} zxYhSRJ#O$)F|u;;g0derIA&w0@dt*!fp38@H}Q?B?D0KQJL}FS@b!@Oz+j;%7zCrDhkxRR~*Ueg}hSN37!M=Ii046b;lfVsw9GqF<7~%Zar|JO0*>ulkYX< z?#Hd+Zi{HIOgtqZu!zcJK^nMGUlISsiY_=!0~#zj!;Q>NbXzM=W?vOWL!iwY*v6hQ zaDMx^0bKCon-{>90Ec64zq;T;N$DlHs{k(QyT(vfp5aMCN1E^wyIQwDC!hM)PH{507hxE!%XeEBz)cR5wRdo zxr`|r__i`uaN5&m)#FB!4~|4*8iH%KfzW!S)G8KMq82+k+&$@xi&~|Kk<+-Gm}oE| zM?ZqPi-r4vTDRiw%xU*H;H}ItWuCxIVVoUP|JQk+$xBF2SHX*#LRK8NPw~`qjcuL| z8JSd?x57Fubilj=SXwMOt2NfwtJP@|3{AYqk?RST_p$Q2%jCf`dNZDj4Rc$LeIJNkG~-_B z^eQ0EB#q%Yi&+qoL4X+%`1-6BTo}lK7??7*RX&-kpC31Xd8-bYcaZ!5U^_k}=B*~r zuc=%GaNGEK&`G!F=V|WjnqvbErT{2^7^r6fK3ktnWA0^8B4KC(a`x;P1HwoQi_AN^ zjZM4>UoSujXJVE9!^4Ix2*Zk}o_8Qg;p1(j_6BWpKS0?ZJ!gIT`CE{`-E(%eb(=}$ z?3-xLG4YPIWw_7o!XV@bR2+eO3|gd`$munN>i7k)n;~nN*w+rK#7Ge}RDGz=)!rW9 zLa;+h!wFRVVb^f#Cpp@kHpby^iF6` zmU+-r#VJcwb3Y&^5GweMl45Q1?!Z)MVggKL^2WmuR`JrCkPNO##twLn*Foqa45iQ^ zX%RGn(6X`CM_;5tUduB|grCzr7-Khuv5yn)mB#|1cDeM$!2!b9NO}Yf;wepnc)T-A zp|1S(z1u#nqxRJ+kIk0#nae*TZ5)qM9PHZ zA*M;#ToG)Zb{R)pn*z6BRQZLE%YcLAS0wMi5pXA$1OH;LfpV7ehAR@sNGPMZMsl?d zoX$Aku;NNktE?5~`kIgnMplcl;`jr~DRT46q2c2)ai#c)|t-{Z0wm(i1U1 z)?C!}5C?Q};&{ff(WRx$Q)WoEQ89oyg0gXc{sme z4&3~5DEPPxI1mkhrzs!dc6K*d9%8QaM||0E6Kq;%>hkI`!0=6f1+(?p?Ca1)SYd>qMyk)JsYShJa8Yp!h*ddn>>`0D9<|;>>G`5KfyLozP z-eK|EK1FVROE`R7hFsf_iwBGD?2?Fav_{c0ac%={Fm`zfPO)LlGluXS_K1?m zKut5Ch9|Y&$XkLEF^;Mbrs5ocn`h?c4H3T`Qkqu**Te51;11>vTRXcYa8Xh|fTI>L z!kca{Sc9cLl16T>$TKMpiwV@!Saj$|1LaPZ6b7KPvF4~kdJ5cpxICT0bD<_ z_7%u>c1M_&9EP$0TpqZYaFe3QaOgNm=-BCKWXmB{5oH^z8}x~|T}RZ)JXF|R;dy3m z!Ii=PT?Jh58*sJ*E;@I1L3jZNfg6rU$S*0mY%=hKPGj$Flknw&(|QY3D`(0#v&%Or zXmhMJVhnK{auI$n*f}vI^>MEh=o(&=HwR7__z$X)&_8K*LT~0tCIpmY+7=TTeWU` zRpU;Z{7)F}T$MYQ=%%5>_MqOztneQ_GnujgW65D}IY z%EdWu^V{JUJ}$$pI^h=b5pE~9LoC9rX!;h-?7~e%xO}V>li`vZirXoY6V~oUqXa%T z(OqV=&9U(6Sbel1qPYZaK^6GgUfWf`C3M0NINQ;_M`tHFrbro*i24nI3&S4?RSd#f za0sXZ37E0qBGSQ5W3`~m#w;FPIZSr^3CL-|dF*Hl@dUU9fkb<)uk|wE@&N9jFR=es zXN>Miz$N)3)%yXQ7jQb{%rb!!Zs;h{xGY#BIKs$EA({(;8>n3g96ODN3mp9%xdpr5 zYq<`&3~qEH7mtuTyB(tTDyjt-h8>BLBBloybg}y3%Z*G)U~6#JRt?=_%Y*hI_1G<9 zwob@JYDIb>XH(?Dg0&AHmmwGZK+f$N*86XD5tT$k zX>3nC^h5R06IhZS1G~OLy_B+y>hGXjZp1FI?H43d#BOt4PN17#5eXkxK}V__LT3li zot3DgCQLj=9>9q+kaM~rOtC)e{y!F@B-#2Qzs{a+F1a)saUA{SG_Hs| z2W~-X_v$YMt`a!>eY@tb{#%_~5ka%~Icmv;(dr8IjDeczjPuIMIZ6a`Q-g#mCN_I9 zOX3=56-FbD!}Z`sDtHCkf~yW6SHaf6VM5sK0JeN`J@D@Wp*XM3Jdz|?GFvTsA(ce>DNGUII*p!7Ei}BWZT@eW!?s|R!^btSq3>pc zt$Raz*v@W+AVQKLS0s%_k~|h$9gA%^ai*TbhH3-3*~ztKf&EItPP7|p!NCltSDj~# z-GXX>+04tZiyLx5&jy=^m1B=0mHoV7I>;bUE4$S_a%cB~*Q#^!Mbb!-&s%R7IHg}^nm&4e-I_ZKgiGt*BkLnYlL!k_HMj!`TAT%tF zHHXk_^h39R})UaC?*cYQ|K0aYWTPeIt&{` z%^@5>cXCCrcL7{4m zoI{O2DnJ8rxJk-zhSM||?B~_#j$3&tmm!x2aRp^e+MjPBqCa`XCh$1T02 z%aDr$xzL^d9dc*&X)`T4C%+_YNfMVNTds32vR|oUu9r+XAiN0V=PZ;P1G9y30P#;? z=&N4H;W#X&;gOW)$jz^a^b%jbsnlpKHNyGbr;ejj4sUz=%x-=tKPgSK7$C3*} z%8}6JIdpjmom~Z8-hzG$`2f0;E5cWi=O_6kmNw{gM>sv&@aV>Pt3j2S6@D?Q0Xj$M z92JukMKIdfhU6B&Eu;zT#lQH=fQth--wuE~t5IvZ3vIo;KS{All3zxyb!0bfICOc? z9zdxb8VzbbEE|h6Pdf@BY0ZIawO`Y!d8IOkZhl4h7x&^WgRWKu{oSqy(4AZn^u#zf zL$^c?Ab?KD>6r7wq02hv0HjkYWi-)69%qr&3gqJjoN}PTtf1!}H)608$W;ohr##jimS)PQ z*v%`{9l6)%%hJLHrF3BD$`N*FS47k~=jE38k&{cQ2|F!8uTD;v6}fWKQnfDB`Uf$W zup{kfva#%#1l^qBHpec{so|bqo-fM__uqD47j`Oy{kJ;1BZAf)SUG`R+!(Lum((IC z-EirOjyflbEOSG4Nf@aW25L5PHI6vPUV3q%gp?V#=`KnLVTU&Q=&o!6*4Y){XZ zCDd!(fJ;Zfom~-Lz!kYAexyrMWV)noWD+{*20@MkU87`BMqwfj1#KqxsH{lf5^mRR zP%is9bae^cWf}Db&!5owxX(15)v1M*6QO%h|0?;IW3n6f7 zLL}GX<6s)Xff(gNLsXaKIcmkBaQ54j+zPn)1@Y^-JztX+uI|xc_>{nf1K>{X2Up|J zm(+e7d)4HYB+>qj4kxi2qJ<@r_)wb_(fC-0E|^ z47ubgkt;3iBmT2$wQftt+}eXi1(gSuTN2aYei3wo2qBU)GHAC#oB)IWfzSasb>jpU z$->AA=Xru|UaRh~mFIF9aBTxF9RYX3(5HwBq?w|U+j1pdyH%UDlKru;xu0s1dCXC> zITI}t0SUU8$%P7(gd76bFg)QL=g7_Phw_}SL(Ze#Oys=nkvl6^yCme;=ZZ2VKj92%!$4Q|{daJ>JF}!i>WvMdus{{`<0&Bb?_M zx_P;p$fc{0Gru7htnQIJ8Kh?EzEKa8#Av!AVv|q8wrD6LkU5SCqgK&M9W+!e$x;ye zdk)BQx4O}j!%ELKD3|As@Vr>vVT;f4GT>Nn1>?2tQzd7$YG_Y9Aoh)7)RjvTtF8x< zlniB#ARQ%gAiyWl+)jO@gk$Aa^=zeXqa!D>&{I!gn_my{IXs8UVB;(>b*lHvt)1Nt z;=In0Y!J2i0h=xdKb9MZXsCCD^@e#7j@#z{);VtTd%-`u>u`f2iGT|}?}1x}4qo%*4QfmT zZsbQ|rY}Mf0rIKoeADsP4TSYJR-aQI4q+|Fh{?361s8Cm`r*Y*GfOrvP(S;p-_Pzc z;L-rD`{=s_iLDFNSU?2Mw;&nxQIJ+?zcj75n90h=GajQ57)ri>V zZc|@QL7U$S?&4G`qh2`QtE8icjLtd>DfUq~nF1sWxf zg-4)zjuErs2if&lYPd_c!MGJ{(@Wv$H>UNkf-P;gLhx?aMq=yPX-H<0^|#3hDWi*o z(LQh++Ux=8^xL{Z_+_;05U>ARtDc;3d#!mF|`lB&8N}O6f+ZVP!fC0Z!D3Hp{pN+SBPik zuuX4-4%+k6^Hs2ATx*0a9Klvk78}`D3kBLAM}nL^O2AF9iEC85t%*TdllTbC8UtK* z#)iRHC)99{I=Q=LnntpDF}g$6|92T^X@KTy+k+O)ZUfPB%l+7B(Lv1x)N&dWkv!PM zo76PL719T}x z2`LITcL3YE67ALAP&w5B*hV5m#hEfP<>qv{1)XlJ-@B0uuB6q(6Ix-((KotnFqZi? z54XT=UWM+c)eCVe12>mx2B$viASBIioVfioBwe>LM+Ci7 zvbkctfd`T6O|Zcy-sa&}u;nFemu1O&NIhV)9g}zetxoO)4slc_6dyMBXmnrdR)}(T zy&N}rxGj)#S|n5j>fc4%w1zq+6SdwvYOPKFZkgsQZC-xvsFkP7QoS{0;D({U1KiTd zwZH(r4mARfWf3+xAwlU%5)%kBBjI!?TiaVDH#|VgV?a#Z4sg}prr=7NgEqes%2R&2 zEXi9NN0QK1zb7-WbaExsM0M;Y46=cEXUihD+rt``y@y{2@|S_RjVp8X>^GM2~ri4 z^XBNhrK&85$4o;?Oh_UrRM`rGNjf)>GbJn4{k8eLbL8fQ=Z;%^x-7}tO3p;C<|E|J zu7w~$c6lMm269zih}n=)BR6u`i&r@%V{eB;bp|J;#|q>_Nc zQ#oU_I*lW42<4rCHZMAN(Ck9c(g3a01JKIZwIK1CNcnV7MhY~I<#S{ls5uem>x3g& za)0NJ8|bD$!j_5K$R4@TTBEM7k>xG(Y~{S@+}E0avde&L6nKEwa0J}h)!+zRkRKAq zbSk=a0oXPUNmr00l0Cey3tv~-lc8&t;{n>^O|#CZ~n<&1)L8X zaBhFT)svfHsKJ^TyJEC~n;a3LMsiy5EmK}LvFS)9<;sy>jHbJ++{$RHaqL@ac81!# z+}ts9Pwq0*%0MmWypQVZ$+f_gWFf~fU>m5ZO*#+iQ4m3nU>&$|xUvUll0^a$Rv~Uy zg`2M0;BcK59dGkZIxjUpIVdJwg_=QNk*MuiysVzw2t}$hPPvdU`tXOyHk~-93!(m*ZMv}hUAY>jv$Yd4R`6L2Uq9m%6Xx=qxSOa zt5;JA4zLQ{EV9Bj=CG*NXM)Un^@ z-0D2*Hm@>w(4K!?l;ce#?2X_`ImnEM6RB~WO;|G`>hkxAN%9Y$Qy2UZD#hr7!3w(X z?(N8SpjmFE-rIltLdmCgdtHV}7wF`aE<*>Q9MGxWA1s;rMR?SS(6<>*BY*p_0TiGo4`F~u13V+) zQw5JJOL*qh;NOtmh*!Zgr0$TQ+NCVQx)>~ZFruRzMGYO09R2OXh7Q1~EHq*9BpaM? z26XzBzeXo6(1|HrhE68gxIr*-k4`>C$B2&7tRAOKD*pCipo82h`q6+-KYc--JjmaK zk6Ie%_$=htYWG1d!zZ#VZbyCZ!A_53EgpK~IRX%MG`@Ylfs+9aX9ZAW13$7K$<#n} zL_|DA$5sckC3hJ*@+aAfv_E<*ehnTLH&S<#(pFh}f7k%(i-cNNj?}u6BO*Xi0G;5o zI+}g_eHA_#qvgP-9h}T|dNK!{%v>^1L>Ss7A0IY+?iqsMyTCUAuH(Vk#(N!-n=5=4 zQp>5j_<9*W%De4I@gX8+_XyDJ_F=$Nu)i7(aYs=m6fK0037_EvO!1kY z&Z+#4FPGsH+5jl!z2_57kLM5oka}ACAqh|{+&*jwrF*1I@LkYvg+AZdU_A^0LTP<} zEwqzUd5xF3%cCWNl0AEbqWdC3yxy(AAM_eadVkmu@<6C^kNIwv`Z%_j6F^~ofh{zW zQ+bI`R{@kz;v+z<0Ob>e7UGrV zSFD#Iq#-|Wpd2BTzm8A@dv7Ff>c`IqK0T5#PRo;o5i6*TW7@jM1c2iDB3q~!_X97M zs{n#}mH?`54^TcmrcK8XwE`Ph;nA3T`>;Wj0a0sPcv3l@wh_evB3s{N3%O!fO1vz| z5gDeUd1v2$8}jKnZH&aIWM5-<8pL1u@nHi9dkixw_^xL6jrBV5!D$V-brxt|&wTw| za=a|h0XJRW1E7ezJ{mV!;u`YfobIPmL|`34WpAkL^bB!~b{9u#hbXYDD@ z3z4tSrTdYWp_7svAz$~(LZP0X(X3Y_U0f;QSdOXOK5Y2dJ<|5P#xfA`+Y#-Ek6&M6 z^P1!9Q?UyHvL-*sT-figdU`~A9?^wL!4Z@@)Bw797yt#_^JLILU93Z5+Y#MAuWg-Z zofj8he|2~+OO*vs`VtW>#4Vwnp3hDf7@Z)L@FV4=ZXPy(A^?iqU^M`f;Ok(e;}S~i zBWNM;TZ;>q<;yhD+X3os+#aCQ^V#VF10>eN^~k)=&BKNdV)+abLv(Nz;4}PaUvOO4 zu)e?+I=&_Uz7QXaa)EllxLZN-ygKNZaLsgqK<$Xh<$C)t5Hj5GC2)&@3Amx)KRD*N zf<{$nf>2!O_2&E84{l)adr<#K7Kl|@wnk3fdXtQuu0nq9f(cLYi#Bx zbj;7;vV1FIFJY7S9pLe_U>F)p8X~mAh!7YQZa&}efeTFyV=@Z@pY4P;JQYi8eC8)~ z$WPbd<64pv6zX1kBAyluH++Jmbjy8sl%USr=NmrQTMgI0=Ll$HgM$d(h>u&wsBs~g zYx!5WEFJA~lMe*a{5?ML6dwi|S!#$qP!HfX`S|&U4{mtU$#@C~x3`KRIM>0my1u^_ z!nnTw372J~heQ*&2>0!v@w8-!q#*F2kXpY9!_eY*`+UITu$o-p_A(wnc^u7P*J;Q! zF5y|I-@05cghxH?0Zo|u<#SpwL?@3^U&oG|IQ^`l&nX}*yI2m(QU+Ven1l-5*((8GdYSg{rzDB2rJ4PZ!olBLp`Vt>NNu7 z%Nn3LQ*d{HLcI(iFpd+Tj?@mI(~4maWQmYuS_<|NgpY3?git?_fe`LGPij#}Q;j8* z3s3Cz9X2l+zP#iV%4GVTTS~Jwt0$QS}fC0DJO1OR45CUSxbDB{Fc)m2dQ2@ZD5V2@PAi6?GE*iJ2h$q4{d_|Qk4@-Q&(z`AU`%?Xa01W# zjQ*8FNSDErO-@iM@c^FFGrH;)6K8Zkk1z`!ZXX6f6@Y>12xLfs2yKV7aRIGM&T(EZ z>N14FsD|}3a(@J!)(g>P4}?T+1Plg;_WQ#I5UzQM6+KYI0~CZCK8RQA`VO1d z3SV0G7s3N47Vz{q=>VS76S^?U*hpm)UFCTDu;GJSJ{s?^Vk)3+M>LjG%)BFc3Lb993vo`I9iJsY+1a07zhkDk51Y6SY zV3)z8krwD;l>GuaEfsF?i02fUj1)h9xD6obrb<6z?wp@qIudb$uH zF*5})X6(1wX~A$qNYqSB_^>wr@nJ&7dh+VaV*83LHmI0f$c#IraO)1`*PW#{14`5vgPdW~RsjVq7D%5KKzH?Bz0q zKm|pFs;hg1@^=t|)~})M{`TP`LWQdhIcP7{1|r1#khS$)zmQk$(XKsTg-|L*M|E|3 z!r$c1k80j#0tNl(Mlqcrbj)8xU}#M6-XT)9^vu4r|X&kF-W;p>xz-x$6+J(ZJdL>Vrje zdR8Z%)nY%!a3zI6Zyz>njFY5@OS6lcijW>0 z=f6;9cV%w)>HXuK`|U0(7B|OSPDe8C)o{~V)0MPWXuOd4ZB?HIlMxk~UH3_Y1yHvp z$6C+^0>3eK#$2=|&Fh~R!jbzz%D(?W6l>V`U!;@zCo*~%F}9IVH(mk_%HKm5@AH%qJg}f7i9a30lHoO?!Tr0|9e#)ef=2g=R zQ7vd6Mb+J*47|UdPK&0V`9jEvpxFStLufYKG;#`sZcOZHVD0zVz@W}G&VExvr*-T( zl1ib!N-PN~#QV|i5p-BcBtPOv@<#6W%oh3Ab_;5%0^ZOxs z4zf2u21=|-#^M^<9Iv04)$Di$P)$TfPPpALeOfc)(m4UQoC?kqx}^p>vm6b88MKqC z$ibT@T!ms8=*xp{I+K>UH44ZlOi)8)E=k6!a?pAP*5(Z?BgQ6D6!RbZZndhL@ALvbjxOqaX)Ip1V zFYG`&t?GKo&qU9479z>g5>?7dlS*Uw>}F&pU_h;A~!x~qbtMM~vmFHOnX#Fn(Xf6q~ z#8D;VffmGe>e}4ZSd@RStKb1NlF2&<*5Lhtww7H5XjPz9R&}GufGcH>iqTao){y^* zrASpJ=x36yo@eI_)_LLi zN?6fCj-8A@$hmz+@}%$_ID?C3Lru;_xMnVk@^vuXAR6FM7*ixr+j9utT^Z765ssU# z3o{c5C-0T+6N31yFKoOpj~9Yf{GMC{clzlM`teg-^Ity+7afXMD1mrn>7-ok z(fNWsug*J10krXec_<{dkrZKe-!=YQy?g(9y;Dxkjeo-#l5Km?E4H>iz4`X~8I1Fi z_Jzw6d2Qy)`!cmX!D(7ov;!qtKm&`w9qO&5R7Vxz(-U3~RkScfz&ozK@$x}?7JQka za2TS&!HITv4psais7R02&#HO^RbE$>N)d@V$_X6k5H<+XFdkI&h;D#3MEUSlD7{oT zG65$n5|`C8163n-D*B!WY9?2*EW+7&#ryXwKCZ&eNb|r(_T2%P*GjDc#An1UgSN48 z5CqUvWPTf|F!-2RH_J9)C8vKi0p}7MGDajH?4$U8nbpo&fD-s)KQaE zOLK1hK1NY>LrwDCWv;-K7sR|z__zwTz=#8|`L=VR^I6fn_&f-~dG<5n05)1}RjUo< z1pv40;b3<|i_S2?1>ho2Ha!0cHH_MyD-OYeltM-;MY~y zMYDlj*o(|%JGmP2@IJif)ey$+%vI0{xkwJ9YnH}R}t9srg zEK<*4Tk!lgd|U<_7@q*!QMYfllbazI=@z!eQw{T`geLm;lBwpGAK3mdP^J6^FtNr~ zl>YxC%gxC0Wh$5s;HX&rIM++diz0Nz$5pr`R|anB2)C1)A?Km#NSrKaFS1X%QRo+_ zB|e3)9e8lypm*3oa~aunpCtrd5vk?8fhNJ$66bq#QHw74xC}Hk;~`gc09rV?7jl#+ zYstJ9{8&!8a&_fA7Taih8eU29aNi;%s;trrrkFKxGn{Yv{d99wq9D>!!xnJUH0B-eEvan}-~<;5LFGSNc#x*H@lgR8Llg7EMDPOij=y}TUlha;sKH3|%3 zMpXGE%#2^l^ZaL4Zswq|+EwM^7u-6Ah4vG?DEcB35Us^^BiWAS?!zp&?Hq)+MsN|S zCahTziZM%oO)#bA>@n4V!nl#YT$k*@j{V4u+#1AS5L=_i!kriD?NFiAtJ>;& z`H{BiH4@i{!H)T^t)KxFyKUY5O&UKJ$s{9s5o_PYTyMb7dKUVC_t=^~p{fGXXV<^O z{qNn3PdX{SoMM^%;84^=LZf_l#X?sY(T}F^5m9n}A$)R)NbM3w@g89`J{J+aT4ija z2;-`H#7p=h3TZ<(W4zOGG0t^yK}~98(<9A}O{W|o)!s*0X$~DZ3iBWzOW3Iv4F{!2 zJW(ZdNXUGz4vi*q>zLOs#;&23#t+n3MCJ(_(=2f$eStU zfL8a?|NPiKSzGimYH7pP__%o@b{j`bPE3L5x) z(1D;S5frqP=5-Fj@;&gT5nEN>sNCzw)Jhthmm3i0Nq5P8w)v|CMyNNAXin~t?19Es zY-6*v!D3pqU|85^f19qYayO}`TcxUE?NeYlFz!X{4?&OKBS23!5YunciMo*E$q`A|XFF`}T!fSF9RkK8C$&AA;KTJv05?z`*TR4>obye%usO;R&1hFz^y2JU;(_(rQBaO(+0RZ$zaZK+!jlH$`Qvu|WO z{?P}JW^17FIlaHU!p4wMK*_ExsXYJGD@%VM#8q+E_=MVlRhW!&YSzQmx)Rk3 znFB_#MFS~tvWo1sT& zE0p$aGW#g{zbj1ufA)SQ^?VBXmO~Ua-`lo}Cuu9GUck{B4k3-BI@MmHG(-aD1yk(X z&KesHC*Cr)i&4k7r&%g$n?t$f;X6JVTVM}V^3!U~q=Jf5ImRCI{-XGP->=Fi@&kz{ zH_UYZN@QOtTdsW9r)eAMk_7t?P&4K@M#GrXtDB;Dn3jRIx0JQ8>L@z%gywB$F{Qn3yULHFeM8=^XW$+&2 zdXI`^IhoGad7+MGU zo&UEZA}4+!D~UBIGWOXQCP=tjs^~DonhTheeRSqpO1ez%R}a8aM6N57@h8j?mvM;d zF}m6VEi#F!6ao@uLC?B}{VQ3n2Gf z)!DLFy&30At`qVD8e5Z`+&FZ`ml$6+NRc?M>>1d`-q4U@KqCAE(GpP;Pj;TCXp!Iw zj|!8@RdRJsO3?9u=!*7)Qi{gL#^HMJOeU!gz<1y3`6tA0jmyCiIZXK1h(~5*vjw3w zeOVQ!oUnNPr%;0^O_fT+eN&^AHPU`-by;R1%K2-_>u|;-55VV=oOOO`k8qDhX)5oi zsUh<6Af8<{)E~b&?J=QV<7jT6BuI@7HY_w@wK6nN zJ^mscD9YkfGW?#Y3qI`w^ZeRHfkHymku0q83&d5$CXyCw3@r4f`Owa_rFrch!(NJ) zW-qCt4C5tmbRhvI=y|U`8DCi$5dAMH>4U#T8^arjc6}q7QLRfZpSztJK)}OBAk-q1 zO9_=nDlOvrhy&3QsFHI)5rvE`%V1<8hkN5Kc@d95kX$-@clKw5=x4^R{QdrSkn^x^@UWfdO!ZUsoA5TiIn9uh) zYnF0~U{f8EaP{*iVu*;ayBa>l1GNb)jFKkt^e}x!`;7xHxZS^%v?slGITQx%gUISkv z{nw<gW_Qh zFp+fiDHYWjof8b@GAr%U5y~o85hVl<-r{6-MMD;)Q*6;_KUqSkx7h2-CzTMfKyP^PYJ3`*}W5I#w? zv4-lwk%>FiS_l;Hoya7BSTgNWzc8?ekPNpa>#~;jkzQybKJ~8tVTPUcCqoH4^$;yw zf(24C1@>vkgZWmr6Mx6<4s9$$Cx_8qu8N3wiJub|2mZM&4`cPNL_nmZd65Shl|ZA& z=Gv{p%gw48gnzE@U7^TPgDZ)%|0H`RO#1+J@sE81PC-FOeYY7B5xO&LZhR{VVDD4sIdB6F9jbS@p3ugfDAUGQ zcY75rMIo3?49Q2nnoR$Xu&e+?0E2g9B}b9k<%BozNJTC!IluRp*)5{*RecLW$eUvg zSJXH0h|gUnlsJ@=eYsNAdECLAp0;AiYj@HuvxD-3t++Me049iNPWE|{kIYe~?@iu+ z14@;I10?=SjD7zj!k_I&e>j&2q)qiQ@{bm!uA36a@lCOk;Oqvq%Q2C4Glqk*Z*F5) z<~C8@#AQktDJpF2TG~!R4`12tBP&lBm#C77s&yASXTtm9PjKVTuhT6{I&r$p_3GjS zwP;IrW_(K`KYOo4IiUqIBCF`YO7I8$d*~mJj8?w9&4JFo=A2;wBxC`?Rb@IE1s&~% zXo$8-%s;WNb4sBPG@jvzdB?H#`=a2(jZuV?M5U?pm+cA*YdM0?|sTf-=s zK}6DnsUgj0$l5(};F43?xO|_hf7LW@C|XuEaVnSYymb>ra|6XJAk5VS$Qd1C?}Mm| ziYp)oVs`gB+~SSh7yKZMMYJ!8^)`!a_eN{cd8S$If>|;N&%qYGk`Ah4+HA+;l-%-*JtW1aP80WsZH%=aOojQ&QUSP{pCQlP?7U+` z`8NIiSZ3Pp-ZsZFR6gSRbArakj+<001nA8_vc-a}71hX}J|`G)I`ka>GQ)0XCVkSu z`ADg9_$a7LGWlPwQuq+BuV~h-aSs7pQdfyQ2((-b%WX?pU6&SnD6;%Yo>5#N`R803 z^TDpKK?}8>o)H#}5mY-6WPG`1$sueLegb6%m4h&Hb<$h0n|;um;bG}FNj|p3bZ}fB z&h&#EsZfG9F@Jqyl4LjQj5Vx+A!GE)0SX-Vzb>D9gTQL62C;LB{Xj|*U@@KZKi$8m zZt+T8JIhnO+uOg5V{4YmZF%QLLAe~IZQ)8;#StcTWio+y)JBaQoKg+^zWk%dF&yLr zFq2>|T3M1sMLUbIoTZuOw4iEw^*zqz@qhKcbY12_kCOYD3H`+Z?58dp3WQOWmRKl} zk|tWRg$aoMORs4i-{lpZ^BMl)AwC`+gGs&L(2f$>)g8K8>ij&G@xegQC$|Skcn&%W z4_}mKZ0o{0KLmt_!3Px*lx4DZmUW-h#Mta%0+RWR$4)F+AS|;zg?XL}nQBT&fZMcNA?jA5PfL z$=^Y+4hq@29h$kpO2vGn&Lpzcmgv3Cro$?71f%@96h?nsV`k?CdKs@nE&-&?1mY2D zPDDSYOUFMS);`xsYX)TU5(yBH17LF8ck-$V6DsRdZ1x<7S4lT)xlaKsm%Fcm=i+a0 z`$tKCG*4$edF&R`5Mk#WLgykIW&1(a?vBe;@1Rlz!zCcn+q%Gf6XtdyOyBNjcGZZM zngTWrfDIV7<;041^*L}xi&4C>8>v@nKJR~UjpM9dqf;1$VA7=6!d|KKt0I z3=(#RMmPTBnfKP8Z3fkd2K?+~WLrn%!tX*rC%%oGLvDhBZv8fAvIv4QX~RjQg@sV6M7?jI<1pAsHXt$;fy(Haz$Oh&aE$GyQTjOy*wGl~ zOk07axhEQ!4u+c;vAtHH10v+-KzCAb_RAa3J&%Bq_4z=Re-+kY^6xGo@Yg~9E_Lpk zp%tqZm@$Ef<4Kdr!=Niwy1N(*Zf&Y%#5@$=9lcwjNUeILz&>BZ>M*(*;Pm|?l`A2y z=2dy#X4Z|`6FoW&p9+N_@XaqwtUvaQl+Rw|5nq>gV#1EiJ>H*uLThfMLEj=J7?j{W z?9AQ=>-KbGu_)y(;LCLI)Jy&X%#D#u8P)#^sAqHefVXrD4L-T(6t=YZnxV$^D)-g0 zF*V|-!?%^bdxhR=K0;V*Yq&Dc%5*^)MmHA96Kli15Gr~w#&RlGE@qD*)D(NoYyx8Y> zS~ckbbSGApT1^oiAM|p*{tS}ddSK6p08L9W&6=pX;Md{4TLPA~VSIxL)xOOg zz3n;?3{%D>ZWYtXZTEla{fQqW5X>LjwBlQ{=r$Se$)R`W9}=pEf0%S`Qwza~QKw$_ z$pCo)XY#OQiuw5Ai0*EfDasUrHT_RhSX^4xE{+^O?+CO zIp>IkWs(k@I+BrjF=95H(^%8)J9i`OE)&WGw9^pw5mNN(ee-(v2+Vu*`MePUlul#x z)0ZIUxG@Gh{=kJIosK#J3U(6t{%MH=X6{D$NG4Jc2oNk*apNx4M&0%ZR6xyNROA-o ze$ZW(Sjp0<%rN(eM*`;rObhwNp&s5%i0*RrL!@q6ZyPqDzLf07GtrAA@}d5}D?aM^ z*IJ(Rcq_Toa4sxJkiNcoz`l&vDr=%KE1ifUIqFsZ)cxu5t>Sk%yFXz3<7zaC`_F9| z4cfwG*VS=HcOBJ_77vIscNck?1*o51s`=DrF}dmI+}7#705TQ*gqrGHza(Eps(JK{#^^*K(d|cKcafd+pfOBlvvx~?wMf<*Rp;M*p*Z&( zBsH33u|eSoCsiKjoOV1F?CKA#7fO&U$=>Bzg_?{XjJr6VTpiL~_WrK*)&>hWR zN1m@?#V^K%yZGVOKnm2_ZnQ#JutLSSr5d{e(U29&r3 zHb<5gi@dq^+zZ}UrFEk-!wwy?GfFHzGYIObvys>D^5SyBRTk${>gf8W-`NL{-&E#=IO{k)9G;qS z%2D{RI5cJ(2u3?g10x$^p8k2>)Od9957o>{(_i7!2?LK z@`UH&2`wQcqw1+oLohGt9(+A`%3WK8gx4G1CTo1gePoXLJp|~-MJ(;6%6Sb~ni=>b zPe^FTM%BZ0Ri8L{p3?CYy_I8kHt}5)+94eP@`53mxFqhVpv0w(|2OS z&_?LtxKFY;JwQ;63b09x4coM^@;Mt&QdG-lX}UsWItufZwB`x7#*BVx4z*kP5Z3K{ zZ}lE*RXfy}`vJ!3p(+hWNt zjyTzid_OOMhZ6rfwKaskG=#Ig4zM;)?d42ttHVW-!ool*ZH}_ogngzfGJu}6x$;(T z$~Yn1MCILICaM?`*QPP}MY<46Q*Nj*%|Q)b>r3p@QiO%XFt84^eVok%u>jd9e>^k@ zYidy6xin8flbaHbQ?Ee}Tg1@QI8))%_2rF#AcQd6QQ1N}uZ1q-4k68VPGcNwnR6X* zwEU=KcUTP4&L_}kBKK}!buo0jh8RzryK>qX<2V8lr2gF5?hnbRiuny1>&#&v`&?k`CY-p!Jh{>;yIbwY9J93dnR@2T1ICi z&n(q`%okv=7;}Dd(5hg0cucuxZDwg=poBk~^JtqYcdhKoWx?(lt@Yv14Jmb|xS4L9 z{V+Rf8$9cR&{_WTswy7~Em&J{{{ zFgY!l?90;?O0)7oyd+5&S87s~PXC%;+4@T`ic_6Yw(Qqyc>=c^$yu3RH}w()OPJwm z|E>-aDvR@r#HRsZ82oanw6wO%@YT>Je#+f@a(i%WeaEJ(Uk_X+_pFiT-UD?{!15BP3Ci z+dwEf7o{a)wa5~)?3Q|o(hyDcVW%8^N~~gXcyTV6`_oRPC!>OsohxfqTj?Na75 z3oZBeTAq~hZ~Egfs?Z90%JuDZiH!=4SW^#EQ{#wTOV;E2jTOeSsyP9}Ke_zuA6a;s zNs;ej1H%b&oLqvmmBn_21Ul@QJqKffISrl$btlMN;Yae=q(V3I zQn9A&5CGI0P?3izShzNXL9x#qFx?jQ)OKV1qZy3V;V0ixP)mSih@DP&5CHMex7V_a zz%bRt-Q=gHF{K=z@A?-qGa0S(EShMsN>aw;m8Y-cNetNY7aTd@2Kwe-va`#E`p>4; zxn&!gFu;FD2-T_8P~?}{O16=DSXQb@`U1oATVH%41KCUpGr2gjEYRL+RIg7aVH(&Z zF3LoH(Cdf@OVUl$2Kw`U_gDoC2p^uH24d)_&auE?cbs(<;R;+E2j&sPFZ>M4((d~5 z6g124Ex)eeV@5>`u>n}t)DI>-28AW}Qz_S{Kq%dN|Chx39c;0yI)n}_>Zb81p0g~v z6~4LMtT0GhW%{K%Y)b$O*Zois`|rZ(pe6>pQhoeCZsv&K&^17LS877=Byu|iqae2Npon`aPnre#YL`~QFA@z9oR^0IkUHzq;;ggl>nUtu)7wdh!L`)! zgVqqx+H}lp&&56mXKYExqV7lQ`mxp_-9_yQgBbV=0v8FG*(c7x_l{DIq>#=rr0bd6 z4m+Yc<8FcwC6$*i)B^?@9)FEtuQ891{o{igtKvO82kEs4DNXCmvK_&|vL zN5%3|pj@ohs}~aXMowsdWw@WQlUyqciRG)q_|T8 z$ERTOx!hz6 zgJ_#VGg-Hf#1gR1a#b=A#)3`C)8$OMKz|LFhIEEc zUivbn{k?TJ{@s}K7ZE6eYRn2LnTTozaG`02rH9S_gOY#5zn&kyM5GM^P|TtDR?F*c zY;@Edd*#m}orQ=`)ZM-Y$V4ao*pIp(LqSB?TwP*OZYG&s&9lT0br$}duD7W;nRurKrGM6|P!)+c(||%3 znUh}%0;11%k1?4sq;sF-hT`gStFmk#FKXH{c3?v-wP71P?(q)pjUJ4=qzHegYipz> zniSWO!JpNapyd)_ZPTKSPw8hGAlTJRGHBQ}InGe(ifYKuoB;kI;;sgFmgmL7ezS(e zcuzDN*b1#0mLtINP~xG)LiY`#AJ&lVGyE`WVK+x=(gWo30Oq6T%ZOV+w@LWv$(rRj zM`fjEpWu4ktbf6l%E`Z`YMK>w`ne6p6rMUxpQh`2oqC@c?r-sB)6PhJVn5ZuGp2N) zIxbE#EnfBh#VlE8jNGvUhYT`^$kcY~|7Y zlWh~*qjef*o#>3W?ZtxkCHwybSwFIU11v2;BNGKY@x4n5AN~)`8I>-AIL(s4)TQzI z<&9fE_udiQIvA+r8AZ%{Eg5fC;Pgyzg2@ATIyB6uF0#Tepn1By*~EFjqD${#rX|@9 z!=rCS?yL~3>Ft?~G%4tqJUKk_M^~De&g%Tt zd}PZ`R^Derpze39eGBVs5wwPGkmsN-*ri1Cr$H9|Izhb;MAe%LC)xbgH_szu)xxqw zp{LH0Jt+b7`n70h2)&wO4SpMQDr3QV`+O!>+B1|wE7?J7lwwv}qWc}lja~S_HgUw) z>hPG00Jo&*D4Q=#z5DFqDkYy~yM`6ys|rt-X8l@Hny49^1zKxY)bbPj+_%Ls5!nfV zG)3ER|C(r%Ocz&?A}eIC<@;{{)m_2O{trrkmRl7au)dr$=)<*N?XsQhA{GSkStWHhc_|*k{ERWbCWkN*FV> zh^R~xFD^Fkkec*V_WA_nJmelp*z)0d%v+!jRt8FKPNoj%HtyN`JzlmI>@DVDj=QDm znX6+{O@gSFAIlXcA%^|etbmi73MF8)K+!0`V6dv^3@t{5p#+WU8b`ueZ6Y8Dir?FS zR@|gId%UZcZXJnhuvbc%T#Jm``hS`|A`%K~Ib-@0_yXVRs>x~VmP1`z0W2@PJUKn# zf7K?|d-yPr$+^sMt^!LcQ!SQs`znWRoHuZuc1;usHnx){?h0>apBPk1%`~UJ~CJ>01bbCL$glkw= zerg=@?@I!aqGJDG`mRf;pEFH(TBWegH?JbVs72J0pyh>)%x;M=hnt$&DYZ9A`FVHf zb(lEu`Bw<2Er@?QI=l4$W2O&ZnW|J zEW{bJUOPn2Jkwn*QQzFiD`^ zIi6)C&BmVdG?@p7=SG-Pt4ZE;pyrJ-fGd<-E(32p&~SA$BlDpqpX-VtWkUTVRW}Wc zp4UR(>sT)T0u5&zK+y$G+Q|-6jJ!Dfk?fP(t+}{AH@;(3(tmsZw3@%Np7q^Jiz50-af@8TxRLgHoHWrqUeM{l&{3(u+Bt z;0e~ZlRr%9(r`8So@;RKMbD}LN2-#Y&s-!I=hlp)hQNlT$1)ntU!CjVwP=2|9?v}j z)L={HWm%qr5c=t?2gkvr&IPgnJ3z$0IrE}j**=dibUbt=*6La0_GJ(=Hf3$IfCqN} ziOBuH0;C;DP@=_`BP1|waGOL5btNt$Mm13Zdv%Hrr{D>;7?RJKXa@Rsm6yfVbe$`; z9EV!sZ!1%B>8{0XYP1FQRf>1dtUoY9l)clJ>k*`%(0|A8VrpQmZ9jOf;^$i|+H~gr zb~&!b0OGACh`wmC9f1yusZ%M^es5~Ajc1rD_WKYo`@^XF;I`40mOv2h@FZOIJlE2n z2P*X6VaWNduLQOdJ)>%ANR-TvO1T9II9aXt*4~!F1RjeA(Mbqdy?;l2n}sd}ou^L8 zR^cc(FH2UF-gvk{tB5NWLzzszA94GyFefjT`z8x?6aE6jYPo!U45jVB>85_nz~_#Z z<@%B7>r=w!+<5X0!-cT5L9_qgIl~3xd${qs1%D9p+=<YVMF3BIqGCYSt+4l7X&w z&+>}5C$KP36%j|UXk0oaW6*MSyEzsGmMY&M0%&R#(Za&9@i@RbE{Vka4*DG+_G*07 z(P+PTZ9-2NI>2+Aw)hHBiW7ytm=B*IUkY3eC0bsDuE+bY^;kwius}TeSEnbf3X=62 z6fY5oA^qn0qn$`qssu#3RxUC6c8gPtH~#?$$6o~r6{VivHi%zP+xJYvLc^4*oo_Yw zeEoQ%mLD=l5kmbj2M+VClKa4`TN*33xaolyJDo9tv8MoGuzduyG|!GKNOnr77cQ_&my&2vIQpOXj@@PFo$)icvxHjXL2z4!8{fWm8gEw zHuV+ z!8Y7Nrq?OA^wRt|QWb>h%iDEe$tG!J!T)KX$_EMQ*><@_3NQHAy=eYUC} zY>Kn0{0sk!1Zs4F-_zL#!2cwx_!te%?x*|4t zoU$QA&56#PNd_C{)EBwjetw2bW4blhZ!`WxIFOdZDr-69PbRIIbadT>L3QVpW#M}s zgDLTx)jU+m+r7vZa>Vf@s&lxh6m^>WPpP$F8~;nrEK*7rp4~#&rbsGk4&>vwuEH%` zy^;1r&OuI=a$}2uKH_PHRat845rk6Dql%XWx|=hmWpJ?}*9O?Q_HM!{D`u90bbsW- zE6lg@ZIpq@ADUI)_%KA%inZ>upJ>~cIw20An9j?326htFA3?X)yt{6nGzJnE4=j9Z^GOvhRi-qq~kby zmH2$&6fRq|@kNF(g%f(bJnCp-B=t5C5$ev)Py_Kl5fZ%!95TCFUj(mcWfA@`zkW7M z>6;TQJs`Wex^t+kwC0Mef8~kBE^07cY;b>91WIc)N7+c#`L8GiLnk{sCpnj5yC9%s zDM?>0#^dDxes;d2)7|$5#W`d-DwWlUV4KBmgb$E3a9}>Td*64#gzH!%U6wp|2q#qx z@3-bgD&=g65S;i7)=vrj<}w9ix($>5WRI(=S5=ZWd&&qxIDELEm@$Byq`m+|`gruL zq`oiyKFotKC_$g!@I(y{`X=tl7KXhMMnYiYD^wzm;Wt~vKEx}=L0Ko8^xKN712xo` z#mK1oCk}&BHL}p_U)y`896YXd4UR@PzW~k2t$S4>vG1_4JZ4G{OAhxuPzay9=fq)C zs;P8-1L9TsO51KnpzibSO*YgdN%EsCz`*voJ8kZ&sA94B(*G1FAC20O&`bwFH9S*?c$-?<%C(3eX(}tK`DDvY%kXPF*grWe~-@zPh7QuLPoj%kZZ1u%&cIT2mh7 zX>#s#w=oX+Z9AfyissL58YL8zovvOT%;wq);b)F@-vzHSmtV?3B3U@blS}Yp?zQ@xl#|KPPYj*xFaN>ms1bgwgsRwS@@$~Wa$4B>#PH2bX0Ed(i6 z$W-2^zzw9-(4l!(7-HDl7C^;$Cwl|Fz_l}IeWb&KJ+5*1&OOnh{|0zrZJdszfwsmI z^7ac)R&v|g5XTrqUI9k*(;|D^UvE1ex>)nMyc%*Jj6=a5Ms|XiEgMLygIXjoCK=2k z19N3n)%u3YMZ}O#z7ub;g>p*i&L$=LQsOsPife_q))OK1Ws{r5jMn+%8fdJ=5@Xw| zxU@xy;?d$JV_F7*R+MZ3wo$bL8SFYF)hsi6KL-DJ_J@Cze0K*8^;>_6*w=+ovFVRj zPm3=QowWCaFieyV#5?5%1O>k&M@-pLI`1(~uq%lXyUe1Rxm$j>o}>vlJ=Ja#Pe}_Q zWgMW+Jr4zkInY<$G}fJ7uZmIhDw}>rK)o&|PN}E28u_Sz{zW;Oq27X0HE9-&y3!SB zx07XCV?W5P17daID)bk{YW?w|=qxzp0t`&Z6J~YZxE0HtBx~q>w4rY)G2n&*#aT;~ z%CVoRSJBlQ3U-+pHD6cd#=@7Oo(GWb=vX}Oe8w^NPOP2hB@h9P5m5`olL(sGbjmIq zMCR-Aej$l=InB*Wc524MESQX|q2Ee`bf=?mZcKg(isfX8w}%)@3)bCnA~7AVe;(Pu zh)T&_Lt+h0a6fnA|11i0gzxx6yzl)Er-*ADp$)OhF!+patLfPeWoI2>s$^J$$e|iugl_$+ySi(^)2P4U^(OTwo;t^k2w0oUS?8jEZiT5iqiz84sbCRBYRjhAF#9&Pn zURN`bsQI)boOk|3|A{Egqjk%2`{}jh@^HG0AZvgxPb<%+ft-s^VMv|(Y$3qo9|eQ) zpG_+)xPaMM*()}vf&7!WeBTc>+;UusK5Y^Dxbn1Dkvos#_Y`9O!bo3OAGJqjmObQR zE?*8?PlzG}FZ}k=W2EHH^xVh_#Pl&?e|Cig@1`tREw#&S>vC#w!s!XAH80gYp*fT+ zLr%Ts`eibbB#ztYvHct3!JrW#s)(vi3VY4SN|aYJI%npUM9QZ`j2bOm=*5=T zGTGt~Adh@|M~z`no1`|pn}D&PLdL%pHuI9DxJf+iKyqo8@)!*a@7Ojw%Xew#xVuTL z$(SuV+Q}!4`z%EJ;K?WLC5FdXQj!RP}oL zFqmbU(8?xy79GM>#q$wUYoKm-=es81So`{e3J*U|q)!L2>^Aov@xDck5WgJBADihT zVw-DcHH0@3t?3|Vvc|0UN*3&JY(&^pNNFU%wa1Bc6$U=-4D?U0jgI>4B?89#aQxCv z2y{iy5K=;8;7+xT_Dpf5u$0%AMTYLD7NROaHV;i|W_64FiK{`HmGdT1foRD`bJEWs z8);w9A5!P63I7&|Dog{IcVTEy>>MRLM#;yr?w2E`x^9&b`ha=T(XCS3`Bk+?&f0L- zPSUF_uKC8viKW*REr_8!C<}*+Z~14`$T(wQ^DX?7S_cmt_Wn&&EJxn*;&WJ^Tcx{& zbY$)&I6sf9bC4Y07w{@3+8`u7CY@`dX-gXxO=+?9V%CcywisYM4RV-P(zGCuLjRIo z{SDt6H!!3#AUx428f;Fnz(y5M9i-0r&yiJQ+gKYcpx~_rOD%oX1D(D@Ir@NTbtSnY6Dy zoFj3eoQR6?6p8=!7Ud>@0v@-PXYut^eCXaBA zU5YKB*)y%UE2@O+a6xNQa&yI9(P=pP08HO^rGrT!xFWW8!%J;Zd#w6`Mt7Hps=-E5 z2ipQpL_w+7giYm!b*Y}x{011*$96w6=b$xUPjB_5IFqq6?dY9pE z`>*ny+r_gxQzWJ{(!PDy1)jOm`EzJS)wOE-e^ET_ofu!g*G!;>6ozkvb<<0`nt4kS zBIk)kR%1!mDo|6D5U^FbC)29YRVJ|n(V!Y@-Lv+#6#dC&UihYf&%^e6wHW1%U3bb> z@)HAY_%gn0pW9zXpBIZ_)3THwEfH{bNINh`_d8H?<<{QvMJ$(5Ru{ec3tBHKK+;*^ z18D;N!)ar)b7Hy|SyKYXH@>rR-vn4=>;v_(m?=#!ow)307QURA*5Dx?Z}9d@6jbSO zY4bYEmVS?$E7v^uDpoGMxLo3`%@LyLC@KI?vK9KB60w#8bTWwpu?iKfXsnQd%RwprRd^K zGR((?EKA|lhf#7+Ol1)i>1D;nmWI`J0lqr>|MIhhA;p~u$k>2WHMsS=FWL^ST%uYX zf8UJtm*g3G&*Yh+i!PRf&7XI^UjJ9hf7AnVQ{zUCK#?qN{}&iV0;+7wAE1gK_Yk1EYlnA>)k5dD)(-A2kq0GW)S6w)31$DKpL6E(NinfLjMSy zP-aDu*>#J&Jpj^>o8+LO>u3tUjT=V*zYSRnkFdJap+PgP^LEl_FOFXpT|i_6xX1?r z+dapRQJ!_h5WaGDwNOl&3hz?FUo#@ zwme})k=HQ+T|XEUixqU_A*xiYE^MV%5|wr&KAk1VL>SiAbOTy-tVRrgc;G0i%<-S> zAg2m9ASB1i+1P+iV74;>S^{7#6kxuIh^+@_2v!l-Q93OrYh3m4+%CN*T{fENyBMY) zHSBe4Lm$E2ehHkwDzSs=#9TGyhHQ#6;M?Cz`wpierSCVJMa%{IQ1NfVprIG&9ILg! zkyRDR!7el2t@KXVUWH`3I~Jp3F~}>3fMULU{A^rj^JxEn(jf*Eqkrfyr@oHhb-(r% zcIjtumTb?}&uPvWPo-d7_m97$NX`Zk*#=U%09k7ISo>s3wRvQ8Xfl;q;NT|wuQNO* z1}q$+$GfKZUB*eR-%S#ZPZg99&0VU&gYm9^H7nkmW}r|(BoI7Uzs~?Gf%T=LuX@f< zp$92f2gChD&c?R_lp?kV$tCRfhzGRhrvLX*n`#Q)DMfCKcH6=n$%gu9n`R`#s|Iu6E5PZ=L4(42)kG&P+y(piKD{i(@;r4m zE%cjD*9}$o{LC+bWkH8^R@BiX@%Uh%VHVrz1|doOusf7*375?7A&n`PD5lTZL3gMB zz0pLo39jl&Q!(Ufs>bIn$PkA=@d*RxO zDs4yiZX-8u=7_q2&lGfEKY`I0msJCu8n@BNH|^|VMk>U=bk-Rsvv>y=Ys$UE zyueOYuQGCFp1G*Z5r`G8 z#xgl|c28Kknak5gg3Cu0DGFoHe|*1tPA>4-(0omN1k2=ve@TMGA389qSwR&S0cvQb zvWg|sjGWWToS;zZ`4+NO1fVckp#H9WD;j+{lUIW|_s9M9_k*#AV>``?(lFqE4Yqd~ z^azZT9J|~1pCoky`k%tk5=BXVh7vV|g~Ne5f_kEO5Ob43;eH8l8Gvg)4#sqU#%->;GVlOAr#sBCC zW-UsLrvQtlIcp{cRMRLC^frKL?w_7G=iopSBv^dh% zE74`n*2Ddo<5RyX%Z6n4FM3f=Ss-9YXBH|OfT;U%9ZQ{5Ye!Fb2K2FkA`%DpPw=sA5IRx(2+#N$ukE=(HF?gXw>sv9{2nfngB2avx zTjVfTD}BqQ$-!?`Iu_Ubw|yXL63~0! z#Q>ItB^?`V!;-YF6hpr84Pt9B4ncPykPE{|_>c!NU0UHBQ=~MNt!&A8h?HSBtZ3&# z-Nv6TfR^AJN(LZMJTM%Xn*KYjY~H0bpB08nv_V*Zx&|G!h(+Q{6>G&3tpTzeWTFd4F&V=OXP~{weXnsP982G2y^dT1sdO(n!mnE%uBqCmN})YERP?dc9;*mrPhKD zG%KAO(}%--tsGh!bJFDZl-5bhwVaCU2jb?4IB$?DVoyJ&0>xfJs=22Cys_z-!LXy; zFGPFnry9VJ3T4*^I-%9(sSB{9Bd+>KLa8%)Q~LEG4l0g zrV}1sdt%&jo}kMqKSko@b6+YYT&0=|pM7uvGxP8~m7LT(dz^Tw6Oyc*3RVTZaUx1W zF%#$5j+igR)v3IoGbXktWC~AT%l{gGxLYMmbSdC`DSVNaoBR4ulXy4ZTsZZz~9%&>fv zveik|=52-@NfvQ-8%CHsM?_-1KQ`C9hmTr>%YxsxySB)LR8Zyb zDQD>8KnI@)h1@WlK^S9}56z!>cl2AdoSI3$IxD^8diD6SAy3}vg9~H=tsoDN_V!hO zS@#kR#K(o*9;z&5tQ7YysSM5V+MYCk~WP@}|(7nDUiMX*}#7LVhX)aeHd(lRwQUX?FLnxo~)PiEO1kqPhi)qB^Xv zozHIH6vRv+y|7XJqPsw{^AmnoTtp}|yFRDjTU$*L%1_~=xq&l$cc@mKKpN%)HJ!t8 zHRT@Kg|o%Pq|m?a%gQ8u0w_#Z8l#QTfq&AK;=A6;@i;=;5QH06=vP>Iock-Ltu4?! zLC;SX$W2I-2Zx0+Y_~Z%!sE6k-Y5w^Mok>rb8)cmbTB%Uwm7FYpW)83VrF{;^FvRR z@8h5wOyOA&8T<>iA`)FE_GFvn>jbgM`LIP`@J^XeN&KPh``1|iEw~Vj#)_v}tcv}9 z1MADC<|U^9wAl5!T_+(?XQadC%lYa#41Bi~AzB)JKEkbJhP8jg>Y#5KYwOci1A;A=rec+a5=ZIH%Ns1WHw>%!kdTqYM#K=Bd| z{Xeb)19(w{Q5deQuov|#dj)Z!b+$?HZHG$N8m98E9*Eaj0vNvzFPjP#swAJq>qCcxW}wka%?*WjbFDW4&lcA(LIKH_UPtBjErMq5w z@9S7%VzwB>IO9A;Dtm&sxz0~oXE1{^rXP)!0KLgn*6xt~VzAAsH}Btzx~8$K-T^jr z!86l1jBejyJjf3#3$TUYGuh?He_0n*{ug)bd-7 zCkcdMN@Q`}oL}g)F@BIkJb>;OCOiyHx6v^%%Q_f$LVdpRu7crSev+Kl@c;UqCJ?gzgiqqU{o1~#-`k2VaQ1JGp4^>T_ksFC>Ruv+ zHcQ~uO|vHDpvKD?WD!1PDRA|)0<<}r^x{$WcJ!M>c$p9g9Y?Gogc8kTXZ}_#i ze{1Nb+VLcAo$|Oi_vBZrbuc`RD-^so2b_tn0>iB}B+>0(VdXrS)W49zzL&W==26J4 zD$l0h<$1*gcJmKQ!1kZ$Y9AgI{Zs~J{2za)E&V3Jbd$S`g9KH+o);~#iX71Fu&X#8 zPl{~@cXR8GJ5F&fgTd=en*{U~tcn`~shw70m)7Rm%E0ygV1W%5>IKWuuN`F{*i^aO z4}Ay!n$J5tY02F~^e23eK(2>NJ>+8_byx}PuWjx6-cRpKAaZ|TX>TnyBs%{XS2YXO z(-Uur(_^=oXee4l5!RJ3-W9EQgk-RfL0*#0(Iz~8C3Z$B=j?tn0;+Q2sSBGJBGdm; z=?5(0=Zo)}3^-5(P10X1NZlckQq`2tXUlT#B&uHw+#^82%COn*zgr9NaYYwp;yB8{ zXPuA^>Dn?tzE&^Gj@)eOm_64|tmI0wArriaw5L}`8>nnwx;_0rDTNxjqXHpsxin`i zJVVfJb8g|UT>t1im{kBxtVTlX4Zc*Eo3=(ucip^?qQ*m+gRvw!Q7r*Rh+;bSPLT7l zv$j!anp=iP9;f4)b%`FB^%{1KDN^xzlQWB`_<;*;K@1kJL~i_1*ub9P$gN?S#w z{EZ4;tdjS?CT^N*qqi&fVGqT{2kJ_%8?^w8D|-3O0jZ#*oasfWMvYWTl!Xs#Ts;pHD8U+rA(z{?<@ub#6A} zX{IU|6$E+|Gd6%jM=aIYqe+-6idsWO{As_6TXx7AxRjow68fO+dW=pex+0!Z&3R1h z*g8$}4yR+_YfYjF5=~w-opV5XDt?Tmq!UzhLgAimOre8Zc+Znk)0sK@%JG^Lye|Ry zAKr(}tp@YPGTZa`-ralgLRXg99*pV|2Q!Xq+Lp#6{&!E&hzCOOG8D+dCgA}-0hXq| zbzBoWZ^kBRY>&6KNjaA-jsgkaW7ZUX`o3kd$3?O+N~KqB3ZJ&q2VTxm`5uTejcy7N zFt5=(&t)2uHr^TiF8q_fWI@O2IQP_~lxO8xWWefk)R~#HXG1PjeNkXIYw|5MAmgFV zFg4T;Czod?l)59~L?d$I`a%(T)o0xT=W4HcqIid+gHHU%cI{)&IO-V3YalSljT(qI zy^^E>4CX16@0GvYy@ws!f-E;{RlT1751PctLtoCkKqbxd+!Qa8n1hQY>CUzl~j6)*j71S0G4@q^Sd8i9hQPX(}9>sh}k~+JU!WU zmx_#0x}s4P=J>27G@lzIL>WXLhDZvwED&mP?VyoNjInhNj)ir;a&KJ3&29-Qn1J*? zvEq24-kqVT1&b*6lzUCwAF;=;ex(L|+UHG7Eo{}t|Dbz=l5)ZhM{wbWB_`eXJlkw# zzKIAY*ovE)S8hfH?Jx(qCUN7natYQk(yyBZy2Bx11J-fSLDEsLO9IWXgw&f!k z6pwK3apIUDZ>Nw>72VuXg(CbMOY%8KCiu%y3U4U&M|nOzV#^P*^oq^$EdfSp zwcI9XXU9r+6CpsLwdC6JGVM_rrlsI;=8^2uDPY47Zj-CsP{b z4U}Q-)8hK&eKSJ$o~Euq)evey>`h~q0khr*CXh;G0R`Qe^gOrhic?9TL@ey+QCh}E zAYK8DF+_btI|!&Z5-UxBa3qPEUwj^8tym`M7>R~=+lxw!8Wpl4xm30PJDcaAga`Ib=>|YB5G9olWq9VgOTP1*@VLwcJXikaz3!9I;VUaJma`zv^~+!eOUte!{V+`v;j<@LQ~E?wNk|bue-P-$JyhF z?gW5R=~Qm#9LErIN#mD3mRqA;raVzyoZF9LwJ!oVdXZM8a`q6`y>K(kJ`;;8+e|{7 zK5sCNp^=DkKqh8JN`zo~J$;FPzdM78kAaKp{uOu7sd#Kj+S~P<_K_vT*zvROmp&3| zGjuAV>N4o&yimE#zZ^Nh?5KPWh^*Y(q)H>a#ya4U-DA8l?FVa@Bc>$9{|C_D{uz0~ zdB#VB27Kd~mQXoAU+|bnI_Tu3kiS>y>Hl61Whhne`2X%1hgF?MCbUCnBbOZX2q`5; zFOlK#geUyKZaX3I607gI6WdxWdx;e7e(={j5Fdf!=*`)ocrNIX0+1w{}*tH-E+DkH_|ISLZK8tyUdP{l?e5u&#d&HZ0wivGZ z)7Ymjyxyq9EOHdD)vfbHb{e z3*i9j{muSQ>kAtH?`a5cqr2B)klB3zpI1n;h_-zbrP=-;^Slmq*U3N7s6ff@mxXnO zyR7ZY?d}Yv{habTPwfQm@tVm8&Z&&PceTk?{35aMlPNN?OtTHp&?&a}w@t;jOG695 zbnC(7%_olD2FX=5_(fEYbcm3LhURNQ2E(Oa@(?o1(@G2NJ&lwwgg8wuRB}kqtA8V) zJ@c`lmUZf|4Yy)wp+#7gRG`-Iw<;x1sl|H*3l9$v|9k1(8bPz(mf$QHIT?uamTmuc z9VJxE4I&yf)S~%{Kh57}jJ1Yr*zdmo*B#gri`iGaag2gh1^r@jdIom-^v}yC#YQlX z`)6TF#M9BTccsNBdgp!yFY3D>3+I_rF>tXaNV;KNKqB9;mIe$v?d3KewLo?mH|(_9 zAfWC&k0g{c=AN1`g&#DL==RwtEBUqw9F(I9>L2nCtjl+eiw(qZ%3^Kv;4VyMX`r?+ z_ED9ZwjanX(AZGu_T4oydhiQzHK3($A>Q-qeI_Jx))GU|qIRzo*XYD=Y|(IWY#4vdFK9G4T3~7x;pHClNNY zUIpwurPqe^w^hj3742~XvvBKV*wv_K=Nnu?lZ4nF-04#zkx%VHhWK>uvgw5XDKQJC z`XayHGUWKo<&rJx^2IbI-1pm(GOte+daqxA^bcOn-> z9OtFs)n;|+S85iKE60p&dYF2sRTq((Tpeh}GrgEL?F4Ts>8&-l8?zpma?N>}9T@Z& zwpoun+}ztdU)~n&{p`Ts*>xrBSeZ@@`F_m{Pb%dJgR=WNfg4IQb-{&3&M z7#DHGge0d0Vv98rfWP@`OQg(y>U1HDzb-A`P%GBn>|8k^PnK}+_wH%%l}W4)IZ+@Y z42K^i7~$}JjWjpkrX06(6n$uB)u=^Mc#4;BjJkUYWG9>}pmbifQ!_-A;5RRoGA!q{ zm!vXr#?E_rk_>8t^N}};-nr4VTS3vshs=#lELSmCWZS3BOX5JGl|l<#lW-<6U00=hN=?;8aCKfvYiSc2&MVsScq;vn_g+)>clx~Z zfHl?4q)%+ZmHkCWU&fhr4t`@}7)Qy@+jmgz$1(*&Mk5B!BU&zn0X(ss0PO72hG9JP zd31b?XTDd=n3^);#aMtK!@2jrB6lL$tf)Feb6d)Ih3t0i!-UQK<@r`(3}=2o!aLm) z0>!`h?0h>!d6x_7^Smq=k0atz<`5QM*&gDOknzb`l(9;3YY)2!c81fuS!G12rGRnM z`;{wrEi?DLsEf00mGykY(rR~tfAesW71I$RKLccD#FrRAwiZ=FZACJz-LZncJ7C&b zu@03Eet-RAfRw~uY|Qei4pV%qCbUJ$b<3isYovEqA)hRc{BN{oFs+l`KNA#5PN`xM z*{xrF$QNzS{=EVh*w5c!%%YiyyjAM1dGin?j27z0a6!B-iOwZL%~Cou zqg5_+6M{$)oAS7*?@nPn(Ue@OlxFf)3$+M-6oMU9i1j4$ojh|7_LN644?w}H(yq_l_l8zF&_)N%KNiRSvcM*W z&Rd5x;7$92(ecgTIa{)C%tXp%bVqqK=X6xiaK=!f>9Pd0zec(ICIhkP;+eUn!{_-;Hl#8$XmTWJx6itmPrf-l;$qhve+HO>&MLLn zX)~OS8F^W)$ZQDIA9>pfjB%-^^YiVRh}?XH>GN_HBa$ozgrNGO>0uxN`ug%QX$A_U z%|Dd1-HO~qW;9qOm%<@&W?qCF&GCvK;V4f)yrDB2=o>6m00;B7TQtKwi%rsWy&cXk zg65knS0_!M@vPLBZ0mu5MjGGlwBT+`iW8p7NBnX_M32BQs_`uCpBixq)f>eiJ#1rk zQO&nOvLOTL0Y9uBrpATgj}exCGQ?*kn~$pO$AZlwW`NH~Y)Sc=8E7Z9z>km|Z#B|< zO!%UlMt4|^>_#;9`Hd1hEjJIhJ>QmF^@ixa4K@&l0VT^s>Q)3(RK?C(zxPZ@1K2zk zA~;S>fhfzZR>9!hTa0c zoD$U(Ig`|PZKyvL*u)z&+XCIy_oIf*HCB6D|eI7;c^rY{*dZ zfc4MDtn}da+O(K+)RGBcbT>2+p)JrECvghCE1oq0O$}9b-n=n|`f7B;XW;e5(q9DQ z!EjF!=ede;Bdbz9Tz%JP#Sw1fo+#^eFqg#k&qfNSb)0Qw+X}$0LsbxQOYQRCO?Oyu z5o`FRu=S^MxBLuIJ#?Et*}1^|q6SS=p(${FoTYC$V14>%LaCS&Yrj8GqW*kGwkGB^ z6%fmRs|&Yb%{4QjmsGhP3Etop&u`u!{_ITXHZP7Y%Ubvk11_;;AgEwjp$nA2*csmq zpq6y#?g^AiWKcNgNGxe?lF<_eszkT`S+S=zzmqOdbENjU^HbW1wG3JwohVCYTl03e zRy%Bk5nEB=0!P&p6)i3Dx01UQ)`UauBmPJWtd=IFa1e*!=OIg@8vtGJ-N_0t2$nGX z8n3@=v~X`u>SMgg@gIkBlmE8@C? z(0hQ~G>O1W-cXgD)}gRn`DE?Wj4Y3kjCaNu{?DWUGQ1dJS}RR~AD=c8hVLGvWUmO~ zLx>fT0_8|8%bf%~{zmP^S+pa|^pQ4_CN8a9;oDGa&NV0zigiZIHU`RC@k8t7rTu_f z`&xhUSvV_sRaOljRwH86{dvVPSsuw4=BOrf@z$Id!>|R(AXJfDTSzDFa&2lZapLs} ziIPi@5%%C>4)L6bbw$xjr_0A-H>Ekpcan>!(1AR&O!2JA37&}y(Ax4({?^B&o zKaB8DBbO&+d21JziPqje199K&C-O#ZQHG*9bnXH6Hkh{nYYEhLvFzh<8y1hDfl7n= z0~P7;-td-Zm-JAee8I)x2$PbJ%&v7}cYSqZnP$?Bg>H>V@VSeR2>b0%q|!HES<8aI zJ&b{8m-h3S5}nvSI|&30-vPM{|K`qglEOKe32~o^l1M*S3AZ}o{K09SB#V0kLk_Tw zi%0+P4Eik}v4ZpW+1wcpUa8(pZ3!1!a6SXkLcg{!bY7?+-N`Shll~yEe0}~Q<)IATPyjUZ2kT>wR_EFg?rD~%;kzNOLNoydK|Q^p zvEhF@sn=YB-u@S`>xowa{oPBKm{SY`#Uqo}+ z$Ed!sS5MyOB9kJM++cjZ*ZGs`d!tv&#Lm4CDM(1gErl+(1h~R0>gDdmK2G2N?r>1Z z@EV6F4~poF;z~@UQo<{blwzoylz#&x;*@yBp)Ofj^#kP_z-N+`y=T1*wtqv>4delA zjX^$dl+N(ZujP`;UJF!MWb=4K;rS?p3Twhbz3T;q)UP$dz5%@%_B0=jXu`Vo#&x?CjjVyE= zD|Ttu@nUHah!}Kx9V^Jk$&u74+RZ<*|NusYh|KF{LzOQS~;oyB|~H1WW%^+<`%oQw+Tc) z9i2Nv1EdXWJ+54{-6N%O|J;P!*&Z1{wYNQ-OpOW)eW0hFw@|Fq`zO>wYb_0F&bVL* z2|Cm0bdTNH8}^bu%KFXgM@jksXr?GGTA)P^8nzQ6)WB9nEmPPw{u(!^rWvIOPsWnC zc#)6Peoc(5ADPK3GpACklF`N(bc28W2g#ukk8P!ib+WorNm#sqGl~_l+S0bPXsDKY z-ZuFk?t1%*RC@FQR<4Ty4i+2wAaD}r;l-P4X^eBGnZ!?`SZJYsJ+FIo+Nzh_$K{SP zL;g4O9#`K%7xY@2YU(IiwpcD+VL}1<>N%6<4fIB) zqy{u)S@s+wc4N)(!aN-$&{s5XtK1?@g28?n{!NGr!_SUIflr>Z2M7i_qHnc8m@rLr z8PDVC`eS?Bji=Zo3DA>eWHVTe6be9VSj-z{hI%~*g(SZ1>2Z|*G37 z{_5?!AZ^nG33lw@UMZXH_Q7lsC4usZNCs5w>!5x~S>do!V=2WXb0QZzlFW7oiZU=&)44fdh7v# zAb#Kee?~znGl!cZq<+IBc&YuC`NoU$kz#BQ)) z?oSl?Zjomn`%ZSn*qx~+<2h&R=%%mE?AF%1Qr`@jDuQw7IIRw zj)M~#!B)haMV*C+vh}_l`b5G4W^!d&bVdv909p3vnS9TN5M(#+0qY+W;Dw9mrzG$D ztWwpWa-U?M)o@X!>xAgrMKw~)LwRMC1M-p^+%Tz7+{B`8XHUC%WvrXvnP+p$J&Xj_l6bn~%DTFGV(2zA+$Xt8F!NCo8$dP@z6Pqlik?_U z%Z3h>n=cH}@|d6|0kubm`kmpj5Y&KEB5Srh9lQM(n1i(GN75I=9Kd5yBH0)kjkXj< z))4lSz+$dd#027zYuQdrf`FzIrRYT{cA~7xDpc+^s&%j2?t4O#*E78}PF99PPu=B4Q|OSjXg$Vvv-z_ z8{?F%y@A$7{|Ue%`XEOdMvR~-l&0U0OtN)vs5z@xHPFAZ5UAh=86iC6a8TCyFCYm* zQ)u&#dnhYVGt^#poZjPBAJk-wlYop6SHh%??8+99*yQq(Iv;QTc z2+ytek7#!Comb@EHa!|k(_AJV{xw>HUA-}<>67mmf=|=OPbl6g+`8pTwO7I-EOpHMCaZZZpU$#_v&^}AFyj6O zVM+-_Lua9HS5hhBpJ+3v2}LBkF(n})Ud4}-MQjmEay;o&A!%zPB0EZe7|WTIrxhLc z=B_X8->szyHv#EWB)bu)Pa0876iLVo+bN;SyJInfViJABWgJN0(Xs)apNYo+&o+3n zh-lQ)kK83PEGpp9?qx~JT8M59c=^3Ge=bv7%SYegU{fI|2k@40nDGkRaKfA9wBt_g zf?m|23Kd}dtNYt^p3N=J?cHSCYB{zoD60_Cf2=)ac9lsgI+AQ;>-j&7o$L0X~ow{;O3^LgLT3s}yYp^HGom9ALj3g1i z4D=8`u4;}`KUfBV9e9S*$~gxXk3Lm1 z2uX(PRqTo#695D8_}?U{@5{t3$a!G-ZYFORN33H{VO%rU)d-B1J6HSmKzv^vJ9a$Q z6)9J@ry@IY*u2FbM_z08jm27lU16oZfY<=;`~};u_r!{9Xn0ibqC{)5;kR&zov8*| z5l!*-BL7Mi0VYIzf%)9_m1n*s&crCYwmvw9^DFF4tl%oyew?mdRiqU|wV)XL&LpMw zzaH5J3cT#OQlH;qU%>P0Ql7$!l(=?)7a0)(z`d2RzXLe#d+THk1~h+w{H}YcwCpz!3dHi3`QjLv z{J2sE^`dWC3ny|XLwr5YlxGj&^1%{7V&K!tV~YUHlH6`nO#^oX^e#a=bVKSoJzmYS zx@5#<;8IarK0nu2)fdyV+(fTyj{PILG zjrw1+)o+#-O~+6^sYn*`Zi*c8H#gT+J9tL~;p}AAh@oh(4SP&Fharqh1>c9LEJ29fln4kY? zN7pG_Hk?W}n@Jzy|EAD#!yA`6Z|vdjv+0TEi;5oN11^3jni7z!ufF-z+VL|{>R?P1 zX7;2h^#G7oAiC9)X_y!%=?c;Cs}4rM>90&NE+b!HCQi4GGf8q1F6b$8Re(r%2R(ab z=J*sDl0S@10`qAa_||kcQMe;KndWn^G^p5XA($&izKWED7{;Isv16)!iU1o}yIWsB z;zS|uy}fGR)ZX4?Fpj)-I2aOurD3bsue*a_v?!1)`IOoC35T|@%N60MqlnmXiGtfv zAmz^`Nnqtwn}P6f2v^r~IjtE{Ef6kz$KJA0OJ@6Y0eVT?7Di%?+h5^t1OaxG3vT`{ zq!Z*u{h4tS#TKHvX-iPb)Y-F=1y51BT!GL$w*s|*%PfGs+1}dr@FS)BhY8#x_g8@? zp)0dKkHnkYhx~zzp1bNs4jzZGYT(r^qFG=zBji;>Mg9a&dczLKz=pX2rW;wIx-^-f!h!!jGxZ< zW*Iu`2U$#$9J@6`EB2Ta`L%t0`fCc&n}0WytnQ7k)iV#fjrXL;D8Gwi<}$--Z&o=Y zg=J3N6&QN!RrScaRYq$L5A+%Is(IKfV=ZYYsM!2o zN*Cwc@6@FhTAro!@!LV(7atUmCfxW90NTEhqFzrSX(MdwgKftW-zPWF;SO# zJ`?l${Lxa9VaFz-^jOesoO6nHM}Pn&@p9lpt{+!-Yqq?Xl;Z&mE<^8z_}|NogPhMY zsTpk|Mf;RMG@jwv)%ZnVVM}C$Nf&!8#S$zmTcjiV8 z%IeUPN+%k8#SLP4#e_S`h&NE>n_ULqTo+F=#Ue33qlF(r1Tm0;4IhOIfE&iRK)@u$ zOAZ6Q^iCAtTJ~XWwh3lo>yaTU`7Dv1KY7aHBtp^f@$y)ZKd^w)|AB9`rCb}u%B7a7 z0OUTY)v44cE5E8FVX!@7Qm66&hc8FGA?Q*!!_?>YmtGir@UwVPo5$YnTbv6fAfD+N z-zqfl&raJtJ=XeBa5w z=?L&WGtNLNc=Q!;(;X=)zHPMR=$U-Q>yjxuU5Z19vE0JVjtUmI%3r?d={gSJj=zQK zM#F|lNG?_GP%|gZSA(&)Bv{_ig+2QBUN$AFHIyw_5nSgtV>A|f4+dOUAQ8ode}=! z+^TTlfCKC2FWg`pQ(M1BP1+H(QSN5d_kmF zv}^nWxJ7UA7)AOCg3@n|;L>Mu8(iyQ=&}bj&^bM}`mIP;qLL14(I)mu@@@<7djq#b zYzWx-tSd+8oPWKq=(PO!AUx+d=MikL z3o1fOgg$Prh4WNU&Yaw)_*r&Z=WrVSW|8BxPm*ba!yb!NFKe~KD368}ou0PZfn)(4 zCBblzc;v9B)zDK%giPbaTS5plh4udvS*`e%yS%t@Z;-3i9f(4s@Kpgp9}x^MxVdLv zFaB}nHKOrxfYFdbS{1KF5KXm{1?X^}w9+59TMvfFlUx6R6zeE>9Q%$ZufR}s!h~q{ zCb8~$ChJ%Z`wJ7*ZcIa`g?fG9a+=-+!c|pK(|$;HYo?*!3$I%P^Hsnz^LKhuT7o6D zeo?_zIf!iq4>f6wD2EE7eQ@Lx7E+hj_m~o`5|Ma6)di>CK+`dlEC;h>%U!!0L+>-8 z6)J4(uW%+>l&;xq13#2Jn4*ksdBH}kN<#-X^Gqz_U^4~uj`ed7hu@!?+*vTbk=4IJ zsnCI-MDhypp(pqPJAZ{cNGFQ$I2hxPD@Pw2{y9t8aDJD;NoB9x2`Z_+ZVCyv&1%dT zEQ51U&Xt;rpS1|4MZwXKIRRQ6@HPehj!bf%N1c{Zp>n!mX?n!gH8EI&8nMR((q*~@ z1D2+X)^rYVE2=H#qyIaP?2q=yV8~kg91JwDcY%^WvRxsVnV47z6w$9mY(iSnN%%@} zX+Js*gCHzbASk-@q!lMEw|v`$q72c7=~3Gme(iaq*p4~7Z_DLgyd56*Q(0=^gM z|7$IC+um{gLwzJeGHP8wNhQ>4?>p%=SSFm9@0=JhQ~Tg+L^-m&QbdfD;x|KtYNa9^ zWFQkcbk;E?^M(JbCNTrA0?7}S6$F4z!R)tMdXN1@9cu&WU>NQQY(HI{({%w!hTfm> zioU!NP%52STwj_)|9g3RlE;O|v|?Snki%%NsJ~2DgN)y4Oo{nVmJD(lJP?7(E8#R; zzJ48DDR{^q)B4_1ER4&fnh*k8e8_#a2NiWFa-o)ts_n3^O2m!yWMlsvnwacltW z_o5HP9Gv4l$LF;E7ce}D6VK|6q8%W$aXEVKdzu=O9E^l7E4uE$vG@$@%{bxWl8wg( zkgpD|nvSeeouiw^jyVA;Q3f=saonG_LkQEl$4%!Nn!rw^e>(pqt6v+)J%qkq1#+fN z4j9<;Heq>(D9VK=2GCsK4O^i^;p6*5PEAS>TEeDcA~$~-bKR@wG5ywF@7R2Ky{dk6 zgdY^cGz+~sc}I2rMriwt*I}#ePo0cC^4CTGDvOZiGP|k5dA^au zPx0tQ3G#ccTpd(g*jqZfuKcgLIuslPErm?bi4K)S=sJ5=4ek0bS~IyB!xJ?WzJUbj5L@ z0U{Yv;aFZ`HPu-LYe3`Au+oWz8ZB}nk;ID_cI%SU>fx;V8SWw9CC)LrhgPQP%Z*e28^|B7>nO|lRdvHOs znQ%yET$FV#y(!E$A1^=5c;b_`P5*Z#(m~Pb?Fh;d4;>*%EDlXdj2?T2ML|UL3vREw z9N@4&B~Rahy#Ql{m!pASz#Ap<@RmQgsa`spCgV&Af@{XBa(bf9qT7C>Cy?SF1Eq3|R-K#gN@B z6+mwA!{=E?GJw;DdKw?#7|1?uvu;-W*;X5dwDMhk+14x!6?BACGAltmC7(AvTeb3t z;ZhcKrhKS(iBxz|dP3PQD0YKy-d9k?&Ltjwa6rf6gZ=f8?KUFZO zSuN{C#RGY(+^nVVXz5N^2iJkk8}lCD5s>J-H?jhVQ#rSZ1u!xC0UX#G6jAbS)+3G! zKaKZ6|5IDN@6*)vY>!%nxItpNm=W6E-KzCN0HsTQQ*J=tLL(d=8Q=&EsP!iWVST&} z!h9R{`S|C`l>G(3Jrw0aoV~FLLTOMCI~llvzMj$0r>F=AaexSw&)BLMQPXjbmmivP ziOY&0b6rHJOG5R!hqYmhksB*s_^d!}`e+7>mO7r`3xjE?&^@0r{jkY$-&5UYIvbLu zGRhe{T`_@z_<(9QXHe2SWVx@alGXCOBl!{#pwQWz{KegAC2FcX3@K$hkm{*N)|G8=b)y@o*wo;#4((@%^LMl3G0{%pZrTQ{zUjpwG}|u8^+0f zl@g#f;xryALTxUjnUQR(K6}xzl04*`TT3=e3AqWP`<*0oMUp#^y0sPo#L2Nayu;YU zVjH36$MS{5*p3F52!O|=WsowW6M-~ejwZPcKQ$G0!E7hB<4J{WF-(xx{}(P;M-mGx z>s^%Bgd_-TT0SQx9Dm}s8DELoC;oHSL(L6Pb9Xr9E(#37m-|!3^6ExUm)?T&xH7x4#gF@X zxf3F*#i^P8r3PRR;hL^icrvTnz*;Royj?V0BhRlCOk{sXA>QOkk5ix}%IVC|8Zpk+ zOM8mLHifc?2uKPp>OXlL+vG&oKb@{EqtrF8S!&~Pu=p5dc}3W0arVHH)RHttpAWn( zvpm}>6se}fBZ%f01uN#j@f-!M8hf98f@|i35&3S zvIvz0pDsQ3#kKv;N~0H*&xO}D#sQk#?E221G$hH87ijW>q^3GGlQ+rY*81*wU9?N3 zxoW2t?v%6DgrbO|!FyG`{at<*+E5XZ@v zxWmZ;@k-X8EE3bG#HKD0RJp^H{pG8y`u!V@3~9+kJ^Icu+>vW?pkFUTWt%2x1Td`P zkNy6wYe4VHEx&EGR1Y{$t+8==Iv@CKk}pM$TOzYnsFZqAYxCV`+zx#_IU za~&m&HomuuOe&6ETySA_N0^Kqd^}O`E)ByJT`5`@eDNBZxQImwK^z*Aw^w@s2F&F; zAR2)gxzXK+TW`a&EIRTtux-fvuc-liqd}Dyr}w##*P}9t-Uc#!)NkX?`I-e)Kb8(oiJx^|ZI4Mj{psp<0QKml z5H#9F;056udsJS`1jzN<0#EM6;xIr%J9hS!{N)ZYMpH`8$2KJf7zD_{8J4mR4Co@` zG6S`a6xGp|$;A(KNZ`k}M2OY}w*Q1sWb=dP(6+v8kw`iT!UkWGNGI*q+r+a(o#z?q z0K#T%YP=G9TAXcH!Ce9|6izk_AyM+hK8gO#we37YX+rL$n+!P~AhCLOqSI(v`os1* zZInN;Qkloai^ND@51d_`t`>d0rl^RDB6_=iOR#(Y=@i^f@L)8uzhCjL# ztZTR*b{$5UP`5?45f|a0X9Vss-svWkdpB^nE-id&+p(C!t_L6)9YgJjd52R!z}g$* znNrf}Rl>0}&xOU#K}|hnVDakU0zd-lL=5g@A_}oP37uS zhgmXp%Z;#JADOtZ+#wb2W*J&F1wPFMb{iMUXd7f)56=!5KC)q-hAI%!8DP+5D@AB$ zg!Gje0ou+Bvr}gs>BOM!w{K2sV%7xjTazmPYB)L(udiw0k9MU3p2#V(-E7(vzk?Zo z$k(6X%@`!_nbAkuBd!tr-%yz{`}wxN(SSPx=k1+7`ibOIU|hfstG_8@Ho5{HiMkX9 zuGT@X@cFVo@_T(~I-k9EJVoGzx&lFo`s(#|y@Wv^^wr)L4b6#OsFGr_00000Vq20200Gvq4S Date: Thu, 1 Jan 2015 14:00:34 +0100 Subject: [PATCH 004/158] Slightly improved loose object decompression test --- gitdb/test/test_stream.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gitdb/test/test_stream.py b/gitdb/test/test_stream.py index eab9a19..aa434ad 100644 --- a/gitdb/test/test_stream.py +++ b/gitdb/test/test_stream.py @@ -16,7 +16,9 @@ DecompressMemMapReader, FDCompressedSha1Writer, LooseObjectDB, - Sha1Writer + Sha1Writer, + MemoryDB, + IStream, ) from gitdb.util import hex_to_bin @@ -27,6 +29,7 @@ import tempfile import os +from io import BytesIO class TestStream(TestBase): """Test stream classes""" @@ -144,6 +147,7 @@ def test_compressed_writer(self): def test_decompress_reader_special_case(self): odb = LooseObjectDB(fixture_path('objects')) + mdb = MemoryDB() for sha in ('888401851f15db0eed60eb1bc29dec5ddcace911', '7bb839852ed5e3a069966281bb08d50012fb309b',): ostream = odb.stream(hex_to_bin(sha)) @@ -151,4 +155,8 @@ def test_decompress_reader_special_case(self): # if there is a bug, we will be missing one byte exactly ! data = ostream.read() assert len(data) == ostream.size + + # Putting it back in should yield nothing new - after all, we have + dump = mdb.store(IStream(ostream.type, ostream.size, BytesIO(data))) + assert dump.hexsha == sha # end for each loose object sha to test From 0d22c80e041dbb5d9d985926b39b7bd7a0573a7a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Jan 2015 16:00:55 +0100 Subject: [PATCH 005/158] Added integrity test for loose objects to search large datasets for the issue described in https://github.com/gitpython-developers/GitPython/issues/220 See test notes for proper usage, it all depends on a useful dataset with high entropy --- gitdb/test/performance/test_pack.py | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index 97c450d..d54a74c 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -9,6 +9,11 @@ TestBigRepoR ) +from gitdb import ( + MemoryDB, + IStream, +) +from gitdb.typ import str_blob_type from gitdb.exc import UnsupportedOperation from gitdb.db.pack import PackedDB from gitdb.utils.compat import xrange @@ -70,6 +75,32 @@ def test_pack_random_access(self): total_kib = total_size / 1000 print("PDB: Obtained %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % (max_items, total_kib, total_kib/elapsed , elapsed, max_items / elapsed), file=sys.stderr) + @skip_on_travis_ci + def test_loose_correctness(self): + """based on the pack(s) of our packed object DB, we will just copy and verify all objects in the back + into the loose object db (memory). + This should help finding dormant issues like this one https://github.com/gitpython-developers/GitPython/issues/220 + faster + :note: It doesn't seem this test can find the issue unless the given pack contains highly compressed + data files, like archives.""" + pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) + mdb = MemoryDB() + for c, sha in enumerate(pdb.sha_iter()): + ostream = pdb.stream(sha) + # the issue only showed on larger files which are hardly compressible ... + if ostream.type != str_blob_type: + continue + istream = IStream(ostream.type, ostream.size, ostream.stream) + mdb.store(istream) + assert istream.binsha == sha + # this can fail ... sometimes, so the packs dataset should be huge + assert len(mdb.stream(sha).read()) == ostream.size + + if c and c % 1000 == 0: + print("Verified %i loose object compression/decompression cycles" % c, file=sys.stderr) + mdb._cache.clear() + # end for each sha to copy + @skip_on_travis_ci def test_correctness(self): pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) From 46a4a79f46ab5a8da97714262095318090674277 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Jan 2015 16:12:15 +0100 Subject: [PATCH 006/158] Bumped new version Fixed tiny issue in python 3 --- doc/source/changes.rst | 8 ++++++++ gitdb/__init__.py | 2 +- gitdb/test/test_stream.py | 4 ++-- setup.py | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index f544f76..a36fd65 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,14 @@ Changelog ######### +***** +0.6.1 +***** + +* Fixed possibly critical error, see https://github.com/gitpython-developers/GitPython/issues/220 + + - However, it only seems to occour on high-entropy data and didn't reoccour after the fix + ***** 0.6.0 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 165993f..2a68940 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -27,7 +27,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 0) +version_info = (0, 6, 1) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/test/test_stream.py b/gitdb/test/test_stream.py index aa434ad..44a557d 100644 --- a/gitdb/test/test_stream.py +++ b/gitdb/test/test_stream.py @@ -148,8 +148,8 @@ def test_compressed_writer(self): def test_decompress_reader_special_case(self): odb = LooseObjectDB(fixture_path('objects')) mdb = MemoryDB() - for sha in ('888401851f15db0eed60eb1bc29dec5ddcace911', - '7bb839852ed5e3a069966281bb08d50012fb309b',): + for sha in (b'888401851f15db0eed60eb1bc29dec5ddcace911', + b'7bb839852ed5e3a069966281bb08d50012fb309b',): ostream = odb.stream(hex_to_bin(sha)) # if there is a bug, we will be missing one byte exactly ! diff --git a/setup.py b/setup.py index dc142c5..c4d9b2a 100755 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ def get_data_files(self): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 0) +version_info = (0, 6, 1) __version__ = '.'.join(str(i) for i in version_info) setup(cmdclass={'build_ext':build_ext_nofail}, From 8b4939630a0d7362e5a6fbca052922d710a87c7e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 1 Jan 2015 18:26:04 +0100 Subject: [PATCH 007/158] Improved decompression test to scan the entire git repository, instead of just packs This should make it easier to assert the issue is truly fixed now [skip ci] --- gitdb/test/performance/test_pack.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index d54a74c..e460311 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -11,6 +11,7 @@ from gitdb import ( MemoryDB, + GitDB, IStream, ) from gitdb.typ import str_blob_type @@ -83,7 +84,8 @@ def test_loose_correctness(self): faster :note: It doesn't seem this test can find the issue unless the given pack contains highly compressed data files, like archives.""" - pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) + from gitdb.util import bin_to_hex + pdb = GitDB(os.path.join(self.gitrepopath, 'objects')) mdb = MemoryDB() for c, sha in enumerate(pdb.sha_iter()): ostream = pdb.stream(sha) @@ -92,7 +94,7 @@ def test_loose_correctness(self): continue istream = IStream(ostream.type, ostream.size, ostream.stream) mdb.store(istream) - assert istream.binsha == sha + assert istream.binsha == sha, "Failed on object %s" % bin_to_hex(sha).decode('ascii') # this can fail ... sometimes, so the packs dataset should be huge assert len(mdb.stream(sha).read()) == ostream.size From ff7615321ee31d981a171f7677a56a971c554059 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 4 Jan 2015 11:21:36 +0100 Subject: [PATCH 008/158] Applied autopep8 autopep8 -v -j 8 --max-line-length 120 --in-place --recursive --- doc/source/conf.py | 7 +- gitdb/__init__.py | 6 +- gitdb/base.py | 20 +- gitdb/db/base.py | 12 +- gitdb/db/git.py | 5 +- gitdb/db/loose.py | 8 +- gitdb/db/mem.py | 3 +- gitdb/db/pack.py | 6 +- gitdb/db/ref.py | 2 + gitdb/exc.py | 14 ++ gitdb/ext/smmap | 2 +- gitdb/fun.py | 104 ++++++----- gitdb/pack.py | 111 ++++++----- gitdb/stream.py | 52 +++--- gitdb/test/db/lib.py | 4 +- gitdb/test/db/test_git.py | 8 +- gitdb/test/db/test_loose.py | 3 +- gitdb/test/db/test_mem.py | 1 + gitdb/test/db/test_pack.py | 6 +- gitdb/test/db/test_ref.py | 5 +- gitdb/test/lib.py | 32 ++-- gitdb/test/performance/__init__.py | 1 - gitdb/test/performance/lib.py | 20 +- gitdb/test/performance/test_pack.py | 34 ++-- gitdb/test/performance/test_pack_streaming.py | 33 ++-- gitdb/test/performance/test_stream.py | 49 ++--- gitdb/test/test_base.py | 15 +- gitdb/test/test_example.py | 3 +- gitdb/test/test_pack.py | 34 ++-- gitdb/test/test_stream.py | 12 +- gitdb/test/test_util.py | 2 +- gitdb/typ.py | 6 +- gitdb/util.py | 34 ++-- gitdb/utils/compat.py | 2 +- gitdb/utils/encoding.py | 2 + setup.py | 174 +++++++++--------- 36 files changed, 460 insertions(+), 372 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 723a345..68d9a3f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -11,7 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -171,8 +172,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'GitDB.tex', u'GitDB Documentation', - u'Sebastian Thiel', 'manual'), + ('index', 'GitDB.tex', u'GitDB Documentation', + u'Sebastian Thiel', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 2a68940..791a2ef 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -8,6 +8,8 @@ import os #{ Initialization + + def _init_externals(): """Initialize external projects by putting them into the path""" for module in ('smmap',): @@ -17,8 +19,8 @@ def _init_externals(): __import__(module) except ImportError: raise ImportError("'%s' could not be imported, assure it is located in your PYTHONPATH" % module) - #END verify import - #END handel imports + # END verify import + # END handel imports #} END initialization diff --git a/gitdb/base.py b/gitdb/base.py index a33fb67..5760b8a 100644 --- a/gitdb/base.py +++ b/gitdb/base.py @@ -11,12 +11,14 @@ ) __all__ = ('OInfo', 'OPackInfo', 'ODeltaPackInfo', - 'OStream', 'OPackStream', 'ODeltaPackStream', - 'IStream', 'InvalidOInfo', 'InvalidOStream' ) + 'OStream', 'OPackStream', 'ODeltaPackStream', + 'IStream', 'InvalidOInfo', 'InvalidOStream') #{ ODB Bases + class OInfo(tuple): + """Carries information about an object in an ODB, provding information about the binary sha of the object, the type_string as well as the uncompressed size in bytes. @@ -62,6 +64,7 @@ def size(self): class OPackInfo(tuple): + """As OInfo, but provides a type_id property to retrieve the numerical type id, and does not include a sha. @@ -71,7 +74,7 @@ class OPackInfo(tuple): __slots__ = tuple() def __new__(cls, packoffset, type, size): - return tuple.__new__(cls, (packoffset,type, size)) + return tuple.__new__(cls, (packoffset, type, size)) def __init__(self, *args): tuple.__init__(self) @@ -98,6 +101,7 @@ def size(self): class ODeltaPackInfo(OPackInfo): + """Adds delta specific information, Either the 20 byte sha which points to some object in the database, or the negative offset from the pack_offset, so that pack_offset - delta_info yields @@ -115,6 +119,7 @@ def delta_info(self): class OStream(OInfo): + """Base for object streams retrieved from the database, providing additional information about the stream. Generally, ODB streams are read-only as objects are immutable""" @@ -124,7 +129,6 @@ def __new__(cls, sha, type, size, stream, *args, **kwargs): """Helps with the initialization of subclasses""" return tuple.__new__(cls, (sha, type, size, stream)) - def __init__(self, *args, **kwargs): tuple.__init__(self) @@ -141,6 +145,7 @@ def stream(self): class ODeltaStream(OStream): + """Uses size info of its stream, delaying reads""" def __new__(cls, sha, type, size, stream, *args, **kwargs): @@ -157,6 +162,7 @@ def size(self): class OPackStream(OPackInfo): + """Next to pack object information, a stream outputting an undeltified base object is provided""" __slots__ = tuple() @@ -176,13 +182,13 @@ def stream(self): class ODeltaPackStream(ODeltaPackInfo): + """Provides a stream outputting the uncompressed offset delta information""" __slots__ = tuple() def __new__(cls, packoffset, type, size, delta_info, stream): return tuple.__new__(cls, (packoffset, type, size, delta_info, stream)) - #{ Stream Reader Interface def read(self, size=-1): return self[4].read(size) @@ -194,6 +200,7 @@ def stream(self): class IStream(list): + """Represents an input content stream to be fed into the ODB. It is mutable to allow the ODB to record information about the operations outcome right in this instance. @@ -246,7 +253,6 @@ def _binsha(self): binsha = property(_binsha, _set_binsha) - def _type(self): return self[1] @@ -275,6 +281,7 @@ def _set_stream(self, stream): class InvalidOInfo(tuple): + """Carries information about a sha identifying an object which is invalid in the queried database. The exception attribute provides more information about the cause of the issue""" @@ -301,6 +308,7 @@ def error(self): class InvalidOStream(InvalidOInfo): + """Carries information about an invalid ODB stream""" __slots__ = tuple() diff --git a/gitdb/db/base.py b/gitdb/db/base.py index a670eea..2615b13 100644 --- a/gitdb/db/base.py +++ b/gitdb/db/base.py @@ -19,11 +19,11 @@ from functools import reduce - __all__ = ('ObjectDBR', 'ObjectDBW', 'FileDBBase', 'CompoundDB', 'CachingDB') class ObjectDBR(object): + """Defines an interface for object database lookup. Objects are identified either by their 20 byte bin sha""" @@ -61,6 +61,7 @@ def sha_iter(self): class ObjectDBW(object): + """Defines an interface to create objects in the database""" def __init__(self, *args, **kwargs): @@ -100,6 +101,7 @@ def store(self, istream): class FileDBBase(object): + """Provides basic facilities to retrieve files of interest, including caching facilities to help mapping hexsha's to objects""" @@ -113,7 +115,6 @@ def __init__(self, root_path): super(FileDBBase, self).__init__() self._root_path = root_path - #{ Interface def root_path(self): """:return: path at which this db operates""" @@ -128,6 +129,7 @@ def db_path(self, rela_path): class CachingDB(object): + """A database which uses caches to speed-up access""" #{ Interface @@ -143,8 +145,6 @@ def update_cache(self, force=False): # END interface - - def _databases_recursive(database, output): """Fill output list with database from db, in order. Deals with Loose, Packed and compound databases.""" @@ -159,10 +159,12 @@ def _databases_recursive(database, output): class CompoundDB(ObjectDBR, LazyMixin, CachingDB): + """A database which delegates calls to sub-databases. Databases are stored in the lazy-loaded _dbs attribute. Define _set_cache_ to update it with your databases""" + def _set_cache_(self, attr): if attr == '_dbs': self._dbs = list() @@ -207,7 +209,7 @@ def stream(self, sha): def size(self): """:return: total size of all contained databases""" - return reduce(lambda x,y: x+y, (db.size() for db in self._dbs), 0) + return reduce(lambda x, y: x + y, (db.size() for db in self._dbs), 0) def sha_iter(self): return chain(*(db.sha_iter() for db in self._dbs)) diff --git a/gitdb/db/git.py b/gitdb/db/git.py index d22e3f1..a4f6f54 100644 --- a/gitdb/db/git.py +++ b/gitdb/db/git.py @@ -20,6 +20,7 @@ class GitDB(FileDBBase, ObjectDBW, CompoundDB): + """A git-style object database, which contains all objects in the 'objects' subdirectory""" # Configuration @@ -41,8 +42,8 @@ def _set_cache_(self, attr): self._dbs = list() loose_db = None for subpath, dbcls in ((self.packs_dir, self.PackDBCls), - (self.loose_dir, self.LooseDBCls), - (self.alternates_dir, self.ReferenceDBCls)): + (self.loose_dir, self.LooseDBCls), + (self.alternates_dir, self.ReferenceDBCls)): path = self.db_path(subpath) if os.path.exists(path): self._dbs.append(dbcls(path)) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 3743026..e924080 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -57,10 +57,11 @@ import os -__all__ = ( 'LooseObjectDB', ) +__all__ = ('LooseObjectDB', ) class LooseObjectDB(FileDBBase, ObjectDBR, ObjectDBW): + """A database which operates on loose object files""" # CONFIGURATION @@ -73,7 +74,6 @@ class LooseObjectDB(FileDBBase, ObjectDBR, ObjectDBW): if os.name == 'nt': new_objects_mode = int("644", 8) - def __init__(self, root_path): super(LooseObjectDB, self).__init__(root_path) self._hexsha_to_file = dict() @@ -164,7 +164,7 @@ def info(self, sha): def stream(self, sha): m = self._map_loose_object(sha) - type, size, stream = DecompressMemMapReader.new(m, close_on_deletion = True) + type, size, stream = DecompressMemMapReader.new(m, close_on_deletion=True) return OStream(sha, type, size, stream) def has_object(self, sha): @@ -199,7 +199,7 @@ def store(self, istream): else: # write object with header, we have to make a new one write_object(istream.type, istream.size, istream.read, writer.write, - chunk_size=self.stream_chunk_size) + chunk_size=self.stream_chunk_size) # END handle direct stream copies finally: if tmp_path: diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index 1aa0d51..595dbf4 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -28,7 +28,9 @@ __all__ = ("MemoryDB", ) + class MemoryDB(ObjectDBR, ObjectDBW): + """A memory database stores everything to memory, providing fast IO and object retrieval. It should be used to buffer results and obtain SHAs before writing it to the actual physical storage, as it allows to query whether object already @@ -85,7 +87,6 @@ def sha_iter(self): except AttributeError: return self._cache.keys() - #{ Interface def stream_copy(self, sha_iter, odb): """Copy the streams as identified by sha's yielded by sha_iter into the given odb diff --git a/gitdb/db/pack.py b/gitdb/db/pack.py index eaf431a..6b03d83 100644 --- a/gitdb/db/pack.py +++ b/gitdb/db/pack.py @@ -31,6 +31,7 @@ class PackedDB(FileDBBase, ObjectDBR, CachingDB, LazyMixin): + """A database operating on a set of object packs""" # sort the priority list every N queries @@ -113,7 +114,7 @@ def sha_iter(self): def size(self): sizes = [item[1].index().size() for item in self._entities] - return reduce(lambda x,y: x+y, sizes, 0) + return reduce(lambda x, y: x + y, sizes, 0) #} END object db read @@ -127,7 +128,6 @@ def store(self, istream): #} END object db write - #{ Interface def update_cache(self, force=False): @@ -177,7 +177,7 @@ def update_cache(self, force=False): def entities(self): """:return: list of pack entities operated upon by this database""" - return [ item[1] for item in self._entities ] + return [item[1] for item in self._entities] def partial_to_complete_sha(self, partial_binsha, canonical_length): """:return: 20 byte sha as inferred by the given partial binary sha diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index d989126..83a9f61 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -8,7 +8,9 @@ __all__ = ('ReferenceDB', ) + class ReferenceDB(CompoundDB): + """A database consisting of database referred to in a file""" # Configuration diff --git a/gitdb/exc.py b/gitdb/exc.py index 73f84d2..d58442f 100644 --- a/gitdb/exc.py +++ b/gitdb/exc.py @@ -5,28 +5,42 @@ """Module with common exceptions""" from gitdb.util import to_hex_sha + class ODBError(Exception): + """All errors thrown by the object database""" + class InvalidDBRoot(ODBError): + """Thrown if an object database cannot be initialized at the given path""" + class BadObject(ODBError): + """The object with the given SHA does not exist. Instantiate with the failed sha""" def __str__(self): return "BadObject: %s" % to_hex_sha(self.args[0]) + class ParseError(ODBError): + """Thrown if the parsing of a file failed due to an invalid format""" + class AmbiguousObjectName(ODBError): + """Thrown if a possibly shortened name does not uniquely represent a single object in the database""" + class BadObjectType(ODBError): + """The object had an unsupported type""" + class UnsupportedOperation(ODBError): + """Thrown if the given operation cannot be supported by the object database""" diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index eb40b44..84929ed 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit eb40b44ce4a6e646aabf7b7091d876738336c42f +Subproject commit 84929ed811142e366d6c5916125302c1419acad6 diff --git a/gitdb/fun.py b/gitdb/fun.py index b7662b4..17da4e5 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -31,15 +31,15 @@ REF_DELTA = 7 delta_types = (OFS_DELTA, REF_DELTA) -type_id_to_type_map = { - 0 : b'', # EXT 1 - 1 : str_commit_type, - 2 : str_tree_type, - 3 : str_blob_type, - 4 : str_tag_type, - 5 : b'', # EXT 2 - OFS_DELTA : "OFS_DELTA", # OFFSET DELTA - REF_DELTA : "REF_DELTA" # REFERENCE DELTA +type_id_to_type_map = { + 0: b'', # EXT 1 + 1: str_commit_type, + 2: str_tree_type, + 3: str_blob_type, + 4: str_tag_type, + 5: b'', # EXT 2 + OFS_DELTA: "OFS_DELTA", # OFFSET DELTA + REF_DELTA: "REF_DELTA" # REFERENCE DELTA } type_to_type_id_map = { @@ -55,8 +55,8 @@ chunk_size = 1000 * mmap.PAGESIZE __all__ = ('is_loose_object', 'loose_object_header_info', 'msb_size', 'pack_object_header_info', - 'write_object', 'loose_object_header', 'stream_copy', 'apply_delta_data', - 'is_equal_canonical_sha', 'connect_deltas', 'DeltaChunkList', 'create_pack_object_header') + 'write_object', 'loose_object_header', 'stream_copy', 'apply_delta_data', + 'is_equal_canonical_sha', 'connect_deltas', 'DeltaChunkList', 'create_pack_object_header') #{ Structures @@ -72,6 +72,7 @@ def _set_delta_rbound(d, size): # MUST NOT DO THIS HERE return d + def _move_delta_lbound(d, bytes): """Move the delta by the given amount of bytes, reducing its size so that its right bound stays static @@ -89,9 +90,11 @@ def _move_delta_lbound(d, bytes): return d + def delta_duplicate(src): return DeltaChunk(src.to, src.ts, src.so, src.data) + def delta_chunk_apply(dc, bbuf, write): """Apply own data to the target buffer :param bbuf: buffer providing source bytes for copy operations @@ -112,15 +115,16 @@ def delta_chunk_apply(dc, bbuf, write): class DeltaChunk(object): + """Represents a piece of a delta, it can either add new data, or copy existing one from a source buffer""" __slots__ = ( - 'to', # start offset in the target buffer in bytes + 'to', # start offset in the target buffer in bytes 'ts', # size of this chunk in the target buffer in bytes 'so', # start offset in the source buffer in bytes or None 'data', # chunk of bytes to be added to the target buffer, # DeltaChunkList to use as base, or None - ) + ) def __init__(self, to, ts, so, data): self.to = to @@ -142,6 +146,7 @@ def has_data(self): #} END interface + def _closest_index(dcl, absofs): """:return: index at which the given absofs should be inserted. The index points to the DeltaChunk with a target buffer absofs that equals or is greater than @@ -160,7 +165,8 @@ def _closest_index(dcl, absofs): lo = mid + 1 # END handle bound # END for each delta absofs - return len(dcl)-1 + return len(dcl) - 1 + def delta_list_apply(dcl, bbuf, write): """Apply the chain's changes and write the final result using the passed @@ -173,6 +179,7 @@ def delta_list_apply(dcl, bbuf, write): delta_chunk_apply(dc, bbuf, write) # END for each dc + def delta_list_slice(dcl, absofs, size, ndcl): """:return: Subsection of this list at the given absolute offset, with the given size in bytes. @@ -209,6 +216,7 @@ def delta_list_slice(dcl, absofs, size, ndcl): class DeltaChunkList(list): + """List with special functionality to deal with DeltaChunks. There are two types of lists we represent. The one was created bottom-up, working towards the latest delta, the other kind was created top-down, working from the @@ -252,16 +260,16 @@ def compress(self): dc = self[i] i += 1 if dc.data is None: - if first_data_index is not None and i-2-first_data_index > 1: - #if first_data_index is not None: + if first_data_index is not None and i - 2 - first_data_index > 1: + # if first_data_index is not None: nd = StringIO() # new data so = self[first_data_index].to # start offset in target buffer - for x in xrange(first_data_index, i-1): + for x in xrange(first_data_index, i - 1): xdc = self[x] nd.write(xdc.data[:xdc.ts]) # END collect data - del(self[first_data_index:i-1]) + del(self[first_data_index:i - 1]) buf = nd.getvalue() self.insert(first_data_index, DeltaChunk(so, len(buf), 0, buf)) @@ -274,10 +282,10 @@ def compress(self): # END skip non-data chunks if first_data_index is None: - first_data_index = i-1 + first_data_index = i - 1 # END iterate list - #if slen_orig != len(self): + # if slen_orig != len(self): # print "INFO: Reduced delta list len to %f %% of former size" % ((float(len(self)) / slen_orig) * 100) return self @@ -288,7 +296,7 @@ def check_integrity(self, target_size=-1): :raise AssertionError: if the size doen't match""" if target_size > -1: assert self[-1].rbound() == target_size - assert reduce(lambda x,y: x+y, (d.ts for d in self), 0) == target_size + assert reduce(lambda x, y: x + y, (d.ts for d in self), 0) == target_size # END target size verification if len(self) < 2: @@ -301,18 +309,19 @@ def check_integrity(self, target_size=-1): assert len(dc.data) >= dc.ts # END for each dc - left = islice(self, 0, len(self)-1) + left = islice(self, 0, len(self) - 1) right = iter(self) right.next() # this is very pythonic - we might have just use index based access here, # but this could actually be faster - for lft,rgt in izip(left, right): + for lft, rgt in izip(left, right): assert lft.rbound() == rgt.to assert lft.to + lft.ts == rgt.to # END for each pair class TopdownDeltaChunkList(DeltaChunkList): + """Represents a list which is generated by feeding its ancestor streams one by one""" __slots__ = tuple() @@ -356,19 +365,19 @@ def connect_with_next_base(self, bdcl): # END update target bounds if len(ccl) == 1: - self[dci-1] = ccl[0] + self[dci - 1] = ccl[0] else: # maybe try to compute the expenses here, and pick the right algorithm # It would normally be faster than copying everything physically though # TODO: Use a deque here, and decide by the index whether to extend # or extend left ! post_dci = self[dci:] - del(self[dci-1:]) # include deletion of dc + del(self[dci - 1:]) # include deletion of dc self.extend(ccl) self.extend(post_dci) slen = len(self) - dci += len(ccl)-1 # deleted dc, added rest + dci += len(ccl) - 1 # deleted dc, added rest # END handle chunk replacement # END for each chunk @@ -391,6 +400,7 @@ def is_loose_object(m): word = (b0 << 8) + b1 return b0 == 0x78 and (word % 31) == 0 + def loose_object_header_info(m): """ :return: tuple(type_string, uncompressed_size_in_bytes) the type string of the @@ -402,6 +412,7 @@ def loose_object_header_info(m): return type_name, int(size) + def pack_object_header_info(data): """ :return: tuple(type_id, uncompressed_size_in_bytes, byte_offset) @@ -430,6 +441,7 @@ def pack_object_header_info(data): # end performance at expense of maintenance ... return (type_id, size, i) + def create_pack_object_header(obj_type, obj_size): """ :return: string defining the pack header comprised of the object type @@ -439,7 +451,7 @@ def create_pack_object_header(obj_type, obj_size): :param obj_size: uncompressed size in bytes of the following object stream""" c = 0 # 1 byte if PY3: - hdr = bytearray() # output string + hdr = bytearray() # output string c = (obj_type << 4) | (obj_size & 0xf) obj_size >>= 4 @@ -447,10 +459,10 @@ def create_pack_object_header(obj_type, obj_size): hdr.append(c | 0x80) c = obj_size & 0x7f obj_size >>= 7 - #END until size is consumed + # END until size is consumed hdr.append(c) else: - hdr = bytes() # output string + hdr = bytes() # output string c = (obj_type << 4) | (obj_size & 0xf) obj_size >>= 4 @@ -458,11 +470,12 @@ def create_pack_object_header(obj_type, obj_size): hdr += chr(c | 0x80) c = obj_size & 0x7f obj_size >>= 7 - #END until size is consumed + # END until size is consumed hdr += chr(c) # end handle interpreter return hdr + def msb_size(data, offset=0): """ :return: tuple(read_bytes, size) read the msb size from the given random @@ -473,8 +486,8 @@ def msb_size(data, offset=0): hit_msb = False if PY3: while i < l: - c = data[i+offset] - size |= (c & 0x7f) << i*7 + c = data[i + offset] + size |= (c & 0x7f) << i * 7 i += 1 if not c & 0x80: hit_msb = True @@ -483,8 +496,8 @@ def msb_size(data, offset=0): # END while in range else: while i < l: - c = ord(data[i+offset]) - size |= (c & 0x7f) << i*7 + c = ord(data[i + offset]) + size |= (c & 0x7f) << i * 7 i += 1 if not c & 0x80: hit_msb = True @@ -494,7 +507,8 @@ def msb_size(data, offset=0): # end performance ... if not hit_msb: raise AssertionError("Could not find terminating MSB byte in data stream") - return i+offset, size + return i + offset, size + def loose_object_header(type, size): """ @@ -502,6 +516,7 @@ def loose_object_header(type, size): followed by the content stream of size 'size'""" return ('%s %i\0' % (force_text(type), size)).encode('ascii') + def write_object(type, size, read, write, chunk_size=chunk_size): """ Write the object as identified by type, size and source_stream into the @@ -522,6 +537,7 @@ def write_object(type, size, read, write, chunk_size=chunk_size): return tbw + def stream_copy(read, write, size, chunk_size): """ Copy a stream up to size bytes using the provided read and write methods, @@ -532,7 +548,7 @@ def stream_copy(read, write, size, chunk_size): # WRITE ALL DATA UP TO SIZE while True: - cs = min(chunk_size, size-dbw) + cs = min(chunk_size, size - dbw) # NOTE: not all write methods return the amount of written bytes, like # mmap.write. Its bad, but we just deal with it ... perhaps its not # even less efficient @@ -548,6 +564,7 @@ def stream_copy(read, write, size, chunk_size): # END duplicate data return dbw + def connect_deltas(dstreams): """ Read the condensed delta chunk information from dstream and merge its information @@ -602,7 +619,7 @@ def connect_deltas(dstreams): rbound = cp_off + cp_size if (rbound < cp_size or - rbound > base_size): + rbound > base_size): break dcl.append(DeltaChunk(tbw, cp_size, cp_off, None)) @@ -610,7 +627,7 @@ def connect_deltas(dstreams): elif c: # NOTE: in C, the data chunks should probably be concatenated here. # In python, we do it as a post-process - dcl.append(DeltaChunk(tbw, c, 0, db[i:i+c])) + dcl.append(DeltaChunk(tbw, c, 0, db[i:i + c])) i += c tbw += c else: @@ -632,6 +649,7 @@ def connect_deltas(dstreams): return tdcl + def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): """ Apply data from a delta buffer using a source buffer to the target file @@ -678,11 +696,11 @@ def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): rbound = cp_off + cp_size if (rbound < cp_size or - rbound > src_buf_size): + rbound > src_buf_size): break write(buffer(src_buf, cp_off, cp_size)) elif c: - write(db[i:i+c]) + write(db[i:i + c]) i += c else: raise ValueError("unexpected delta opcode 0") @@ -721,11 +739,11 @@ def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): rbound = cp_off + cp_size if (rbound < cp_size or - rbound > src_buf_size): + rbound > src_buf_size): break write(buffer(src_buf, cp_off, cp_size)) elif c: - write(db[i:i+c]) + write(db[i:i + c]) i += c else: raise ValueError("unexpected delta opcode 0") @@ -749,7 +767,7 @@ def is_equal_canonical_sha(canonical_length, match, sha1): return False if canonical_length - binary_length and \ - (byte_ord(match[-1]) ^ byte_ord(sha1[len(match)-1])) & 0xf0: + (byte_ord(match[-1]) ^ byte_ord(sha1[len(match) - 1])) & 0xf0: return False # END handle uneven canonnical length return True diff --git a/gitdb/pack.py b/gitdb/pack.py index 375cc59..b4ba787 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -72,8 +72,6 @@ __all__ = ('PackIndexFile', 'PackFile', 'PackEntity') - - #{ Utilities def pack_object_at(cursor, offset, as_stream): @@ -107,7 +105,7 @@ def pack_object_at(cursor, offset, as_stream): total_rela_offset = i # REF DELTA elif type_id == REF_DELTA: - total_rela_offset = data_rela_offset+20 + total_rela_offset = data_rela_offset + 20 delta_info = data[data_rela_offset:total_rela_offset] # BASE OBJECT else: @@ -129,6 +127,7 @@ def pack_object_at(cursor, offset, as_stream): # END handle info # END handle stream + def write_stream_to_pack(read, write, zstream, base_crc=None): """Copy a stream as read from read function, zip it, and write the result. Count the number of written bytes and return it @@ -142,7 +141,7 @@ def write_stream_to_pack(read, write, zstream, base_crc=None): crc = 0 if want_crc: crc = base_crc - #END initialize crc + # END initialize crc while True: chunk = read(chunk_size) @@ -153,18 +152,18 @@ def write_stream_to_pack(read, write, zstream, base_crc=None): if want_crc: crc = crc32(compressed, crc) - #END handle crc + # END handle crc if len(chunk) != chunk_size: break - #END copy loop + # END copy loop compressed = zstream.flush() bw += len(compressed) write(compressed) if want_crc: crc = crc32(compressed, crc) - #END handle crc + # END handle crc return (br, bw, crc) @@ -173,6 +172,7 @@ def write_stream_to_pack(read, write, zstream, base_crc=None): class IndexWriter(object): + """Utility to cache index information, allowing to write all information later in one go to the given stream **Note:** currently only writes v2 indices""" @@ -198,15 +198,15 @@ def write(self, pack_sha, write): sha_write(pack(">L", PackIndexFile.index_version_default)) # fanout - tmplist = list((0,)*256) # fanout or list with 64 bit offsets + tmplist = list((0,) * 256) # fanout or list with 64 bit offsets for t in self._objs: tmplist[byte_ord(t[0][0])] += 1 - #END prepare fanout + # END prepare fanout for i in xrange(255): v = tmplist[i] sha_write(pack('>L', v)) - tmplist[i+1] += v - #END write each fanout entry + tmplist[i + 1] += v + # END write each fanout entry sha_write(pack('>L', tmplist[255])) # sha1 ordered @@ -215,8 +215,8 @@ def write(self, pack_sha, write): # crc32 for t in self._objs: - sha_write(pack('>L', t[1]&0xffffffff)) - #END for each crc + sha_write(pack('>L', t[1] & 0xffffffff)) + # END for each crc tmplist = list() # offset 32 @@ -224,15 +224,15 @@ def write(self, pack_sha, write): ofs = t[2] if ofs > 0x7fffffff: tmplist.append(ofs) - ofs = 0x80000000 + len(tmplist)-1 - #END hande 64 bit offsets - sha_write(pack('>L', ofs&0xffffffff)) - #END for each offset + ofs = 0x80000000 + len(tmplist) - 1 + # END hande 64 bit offsets + sha_write(pack('>L', ofs & 0xffffffff)) + # END for each offset # offset 64 for ofs in tmplist: sha_write(pack(">Q", ofs)) - #END for each offset + # END for each offset # trailer assert(len(pack_sha) == 20) @@ -242,8 +242,8 @@ def write(self, pack_sha, write): return sha - class PackIndexFile(LazyMixin): + """A pack index provides offsets into the corresponding pack, allowing to find locations for offsets faster.""" @@ -273,8 +273,9 @@ def _set_cache_(self, attr): self._cursor = mman.make_cursor(self._indexpath).use_region() # We will assume that the index will always fully fit into memory ! if mman.window_size() > 0 and self._cursor.file_size() > mman.window_size(): - raise AssertionError("The index file at %s is too large to fit into a mapped window (%i > %i). This is a limitation of the implementation" % (self._indexpath, self._cursor.file_size(), mman.window_size())) - #END assert window size + raise AssertionError("The index file at %s is too large to fit into a mapped window (%i > %i). This is a limitation of the implementation" % ( + self._indexpath, self._cursor.file_size(), mman.window_size())) + # END assert window size else: # now its time to initialize everything - if we are here, someone wants # to access the fanout table or related properties @@ -293,27 +294,25 @@ def _set_cache_(self, attr): setattr(self, fname, getattr(self, "_%s_v%i" % (fname, self._version))) # END for each function to initialize - # INITIALIZE DATA # byte offset is 8 if version is 2, 0 otherwise self._initialize() # END handle attributes - #{ Access V1 def _entry_v1(self, i): """:return: tuple(offset, binsha, 0)""" - return unpack_from(">L20s", self._cursor.map(), 1024 + i*24) + (0, ) + return unpack_from(">L20s", self._cursor.map(), 1024 + i * 24) + (0, ) def _offset_v1(self, i): """see ``_offset_v2``""" - return unpack_from(">L", self._cursor.map(), 1024 + i*24)[0] + return unpack_from(">L", self._cursor.map(), 1024 + i * 24)[0] def _sha_v1(self, i): """see ``_sha_v2``""" - base = 1024 + (i*24)+4 - return self._cursor.map()[base:base+20] + base = 1024 + (i * 24) + 4 + return self._cursor.map()[base:base + 20] def _crc_v1(self, i): """unsupported""" @@ -343,7 +342,7 @@ def _offset_v2(self, i): def _sha_v2(self, i): """:return: sha at the given index of this file index instance""" base = self._sha_list_offset + i * 20 - return self._cursor.map()[base:base+20] + return self._cursor.map()[base:base + 20] def _crc_v2(self, i): """:return: 4 bytes crc for the object at index i""" @@ -369,7 +368,7 @@ def _read_fanout(self, byte_offset): out = list() append = out.append for i in xrange(256): - append(unpack_from('>L', d, byte_offset + i*4)[0]) + append(unpack_from('>L', d, byte_offset + i * 4)[0]) # END for each entry return out @@ -421,7 +420,7 @@ def sha_to_index(self, sha): get_sha = self.sha lo = 0 # lower index, the left bound of the bisection if first_byte != 0: - lo = self._fanout_table[first_byte-1] + lo = self._fanout_table[first_byte - 1] hi = self._fanout_table[first_byte] # the upper, right bound of the bisection # bisect until we have the sha @@ -455,7 +454,7 @@ def partial_sha_to_index(self, partial_bin_sha, canonical_length): get_sha = self.sha lo = 0 # lower index, the left bound of the bisection if first_byte != 0: - lo = self._fanout_table[first_byte-1] + lo = self._fanout_table[first_byte - 1] hi = self._fanout_table[first_byte] # the upper, right bound of the bisection # fill the partial to full 20 bytes @@ -481,7 +480,7 @@ def partial_sha_to_index(self, partial_bin_sha, canonical_length): if is_equal_canonical_sha(canonical_length, partial_bin_sha, cur_sha): next_sha = None if lo + 1 < self.size(): - next_sha = get_sha(lo+1) + next_sha = get_sha(lo + 1) if next_sha and next_sha == cur_sha: raise AmbiguousObjectName(partial_bin_sha) return lo @@ -500,6 +499,7 @@ def sha_to_index(self, sha): class PackFile(LazyMixin): + """A pack is a file written according to the Version 2 for git packs As we currently use memory maps, it could be assumed that the maximum size of @@ -516,7 +516,7 @@ class PackFile(LazyMixin): pack_version_default = 2 # offset into our data at which the first object starts - first_object_offset = 3*4 # header bytes + first_object_offset = 3 * 4 # header bytes footer_size = 20 # final sha def __init__(self, packpath): @@ -549,7 +549,6 @@ def _iter_objects(self, start_offset, as_stream=True): stream_copy(ostream.read, null.write, ostream.size, chunk_size) cur_offset += (data_offset - ostream.pack_offset) + ostream.stream.compressed_bytes_read() - # if a stream is requested, reset it beforehand # Otherwise return the Stream object directly, its derived from the # info object @@ -578,7 +577,7 @@ def data(self): def checksum(self): """:return: 20 byte sha1 hash on all object sha's contained in this file""" - return self._cursor.use_region(self._cursor.file_size()-20).buffer()[:] + return self._cursor.use_region(self._cursor.file_size() - 20).buffer()[:] def path(self): """:return: path to the packfile""" @@ -645,13 +644,14 @@ def stream_iter(self, start_offset=0): class PackEntity(LazyMixin): + """Combines the PackIndexFile and the PackFile into one, allowing the actual objects to be resolved and iterated""" - __slots__ = ( '_index', # our index file - '_pack', # our pack file - '_offset_map' # on demand dict mapping one offset to the next consecutive one - ) + __slots__ = ('_index', # our index file + '_pack', # our pack file + '_offset_map' # on demand dict mapping one offset to the next consecutive one + ) IndexFileCls = PackIndexFile PackFileCls = PackFile @@ -673,7 +673,7 @@ def _set_cache_(self, attr): offset_map = None if len(offsets_sorted) == 1: - offset_map = { offsets_sorted[0] : last_offset } + offset_map = {offsets_sorted[0]: last_offset} else: iter_offsets = iter(offsets_sorted) iter_offsets_plus_one = iter(offsets_sorted) @@ -895,10 +895,9 @@ def collect_streams(self, sha): :raise BadObject:""" return self.collect_streams_at_offset(self._index.offset(self._sha_to_index(sha))) - @classmethod def write_pack(cls, object_iter, pack_write, index_write=None, - object_count = None, zlib_compression = zlib.Z_BEST_SPEED): + object_count=None, zlib_compression=zlib.Z_BEST_SPEED): """ Create a new pack by putting all objects obtained by the object_iterator into a pack which is written using the pack_write method. @@ -923,9 +922,9 @@ def write_pack(cls, object_iter, pack_write, index_write=None, if not object_count: if not isinstance(object_iter, (tuple, list)): objs = list(object_iter) - #END handle list type + # END handle list type object_count = len(objs) - #END handle object + # END handle object pack_writer = FlexibleSha1Writer(pack_write) pwrite = pack_writer.write @@ -939,7 +938,7 @@ def write_pack(cls, object_iter, pack_write, index_write=None, if wants_index: index = IndexWriter() - #END handle index header + # END handle index header actual_count = 0 for obj in objs: @@ -952,30 +951,31 @@ def write_pack(cls, object_iter, pack_write, index_write=None, crc = crc32(hdr) else: crc = None - #END handle crc + # END handle crc pwrite(hdr) # data stream zstream = zlib.compressobj(zlib_compression) ostream = obj.stream - br, bw, crc = write_stream_to_pack(ostream.read, pwrite, zstream, base_crc = crc) + br, bw, crc = write_stream_to_pack(ostream.read, pwrite, zstream, base_crc=crc) assert(br == obj.size) if wants_index: index.append(obj.binsha, crc, ofs) - #END handle index + # END handle index ofs += len(hdr) + bw if actual_count == object_count: break - #END abort once we are done - #END for each object + # END abort once we are done + # END for each object if actual_count != object_count: - raise ValueError("Expected to write %i objects into pack, but received only %i from iterators" % (object_count, actual_count)) - #END count assertion + raise ValueError( + "Expected to write %i objects into pack, but received only %i from iterators" % (object_count, actual_count)) + # END count assertion # write footer - pack_sha = pack_writer.sha(as_hex = False) + pack_sha = pack_writer.sha(as_hex=False) assert len(pack_sha) == 20 pack_write(pack_sha) ofs += len(pack_sha) # just for completeness ;) @@ -983,12 +983,12 @@ def write_pack(cls, object_iter, pack_write, index_write=None, index_sha = None if wants_index: index_sha = index.write(pack_sha, index_write) - #END handle index + # END handle index return pack_sha, index_sha @classmethod - def create(cls, object_iter, base_dir, object_count = None, zlib_compression = zlib.Z_BEST_SPEED): + def create(cls, object_iter, base_dir, object_count=None, zlib_compression=zlib.Z_BEST_SPEED): """Create a new on-disk entity comprised of a properly named pack file and a properly named and corresponding index file. The pack contains all OStream objects contained in object iter. :param base_dir: directory which is to contain the files @@ -1012,5 +1012,4 @@ def create(cls, object_iter, base_dir, object_count = None, zlib_compression = z return cls(new_pack_path) - #} END interface diff --git a/gitdb/stream.py b/gitdb/stream.py index b0a8900..4478a0f 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -38,14 +38,15 @@ except ImportError: pass -__all__ = ( 'DecompressMemMapReader', 'FDCompressedSha1Writer', 'DeltaApplyReader', - 'Sha1Writer', 'FlexibleSha1Writer', 'ZippedStoreShaWriter', 'FDCompressedSha1Writer', - 'FDStream', 'NullStream') +__all__ = ('DecompressMemMapReader', 'FDCompressedSha1Writer', 'DeltaApplyReader', + 'Sha1Writer', 'FlexibleSha1Writer', 'ZippedStoreShaWriter', 'FDCompressedSha1Writer', + 'FDStream', 'NullStream') #{ RO Streams class DecompressMemMapReader(LazyMixin): + """Reads data in chunks from a memory map and decompresses it. The client sees only the uncompressed data, respective file-like read calls are handling on-demand buffered decompression accordingly @@ -63,9 +64,9 @@ class DecompressMemMapReader(LazyMixin): to better support streamed reading - it would only need to keep the mmap and decompress it into chunks, thats all ... """ __slots__ = ('_m', '_zip', '_buf', '_buflen', '_br', '_cws', '_cwe', '_s', '_close', - '_cbr', '_phi') + '_cbr', '_phi') - max_read_size = 512*1024 # currently unused + max_read_size = 512 * 1024 # currently unused def __init__(self, m, close_on_deletion, size=None): """Initialize with mmap for stream reading @@ -214,7 +215,6 @@ def read(self, size=-1): return bytes() # END handle depletion - # deplete the buffer, then just continue using the decompress object # which has an own buffer. We just need this to transparently parse the # header from the zlib stream @@ -263,7 +263,6 @@ def read(self, size=-1): self._cwe = cws + size # END handle tail - # if window is too small, make it larger so zip can decompress something if self._cwe - self._cws < 8: self._cwe = self._cws + 8 @@ -285,7 +284,7 @@ def read(self, size=-1): unused_datalen = len(self._zip.unconsumed_tail) else: unused_datalen = len(self._zip.unconsumed_tail) + len(self._zip.unused_data) - # end handle very special case ... + # end handle very special case ... self._cbr += len(indata) - unused_datalen self._br += len(dcompdat) @@ -301,12 +300,13 @@ def read(self, size=-1): # to read, if we are called by compressed_bytes_read - it manipulates # us to empty the stream if dcompdat and (len(dcompdat) - len(dat)) < size and self._br < self._s: - dcompdat += self.read(size-len(dcompdat)) + dcompdat += self.read(size - len(dcompdat)) # END handle special case return dcompdat class DeltaApplyReader(LazyMixin): + """A reader which dynamically applies pack deltas to a base object, keeping the memory demands to a minimum. @@ -332,15 +332,15 @@ class DeltaApplyReader(LazyMixin): * cmd == 0 - invalid operation ( or error in delta stream ) """ __slots__ = ( - "_bstream", # base stream to which to apply the deltas - "_dstreams", # tuple of delta stream readers - "_mm_target", # memory map of the delta-applied data - "_size", # actual number of bytes in _mm_target - "_br" # number of bytes read - ) + "_bstream", # base stream to which to apply the deltas + "_dstreams", # tuple of delta stream readers + "_mm_target", # memory map of the delta-applied data + "_size", # actual number of bytes in _mm_target + "_br" # number of bytes read + ) #{ Configuration - k_max_memory_move = 250*1000*1000 + k_max_memory_move = 250 * 1000 * 1000 #} END configuration def __init__(self, stream_list): @@ -414,7 +414,6 @@ def _set_cache_brute_(self, attr): base_size = target_size = max(base_size, max_target_size) # END adjust buffer sizes - # Allocate private memory map big enough to hold the first base buffer # We need random access to it bbuf = allocate_memory(base_size) @@ -440,11 +439,11 @@ def _set_cache_brute_(self, attr): ddata = allocate_memory(dstream.size - offset) ddata.write(dbuf) # read the rest from the stream. The size we give is larger than necessary - stream_copy(dstream.read, ddata.write, dstream.size, 256*mmap.PAGESIZE) + stream_copy(dstream.read, ddata.write, dstream.size, 256 * mmap.PAGESIZE) ####################################################################### if 'c_apply_delta' in globals(): - c_apply_delta(bbuf, ddata, tbuf); + c_apply_delta(bbuf, ddata, tbuf) else: apply_delta_data(bbuf, src_size, ddata, len(ddata), tbuf.write) ####################################################################### @@ -463,7 +462,6 @@ def _set_cache_brute_(self, attr): self._mm_target = bbuf self._size = final_target_size - #{ Configuration if not has_perf_mod: _set_cache_ = _set_cache_brute_ @@ -512,13 +510,13 @@ def new(cls, stream_list): # END single object special handling if stream_list[-1].type_id in delta_types: - raise ValueError("Cannot resolve deltas if there is no base object stream, last one was type: %s" % stream_list[-1].type) + raise ValueError( + "Cannot resolve deltas if there is no base object stream, last one was type: %s" % stream_list[-1].type) # END check stream return cls(stream_list) #} END interface - #{ OInfo like Interface @property @@ -543,6 +541,7 @@ def size(self): #{ W Streams class Sha1Writer(object): + """Simple stream writer which produces a sha whenever you like as it degests everything it is supposed to write""" __slots__ = "sha1" @@ -565,7 +564,7 @@ def write(self, data): #{ Interface - def sha(self, as_hex = False): + def sha(self, as_hex=False): """:return: sha so far :param as_hex: if True, sha will be hex-encoded, binary otherwise""" if as_hex: @@ -576,6 +575,7 @@ def sha(self, as_hex = False): class FlexibleSha1Writer(Sha1Writer): + """Writer producing a sha1 while passing on the written bytes to the given write function""" __slots__ = 'writer' @@ -590,8 +590,10 @@ def write(self, data): class ZippedStoreShaWriter(Sha1Writer): + """Remembers everything someone writes to it and generates a sha""" __slots__ = ('buf', 'zip') + def __init__(self): Sha1Writer.__init__(self) self.buf = BytesIO() @@ -623,6 +625,7 @@ def getvalue(self): class FDCompressedSha1Writer(Sha1Writer): + """Digests data written to it, making the sha available, then compress the data and write it to the file descriptor @@ -662,10 +665,12 @@ def close(self): class FDStream(object): + """A simple wrapper providing the most basic functions on a file descriptor with the fileobject interface. Cannot use os.fdopen as the resulting stream takes ownership""" __slots__ = ("_fd", '_pos') + def __init__(self, fd): self._fd = fd self._pos = 0 @@ -694,6 +699,7 @@ def close(self): class NullStream(object): + """A stream that does nothing but providing a stream interface. Use it like /dev/null""" __slots__ = tuple() diff --git a/gitdb/test/db/lib.py b/gitdb/test/db/lib.py index af6d9e0..528bcc1 100644 --- a/gitdb/test/db/lib.py +++ b/gitdb/test/db/lib.py @@ -32,7 +32,9 @@ __all__ = ('TestDBBase', 'with_rw_directory', 'with_packs_rw', 'fixture_path') + class TestDBBase(TestBase): + """Base class providing testing routines on databases""" # data @@ -65,7 +67,6 @@ def _assert_object_writing_simple(self, db): assert len(shas) == db.size() assert len(shas[0]) == 20 - def _assert_object_writing(self, db): """General tests to verify object writing, compatible to ObjectDBW **Note:** requires write access to the database""" @@ -126,4 +127,3 @@ def _assert_object_writing(self, db): assert ostream.getvalue() == new_ostream.getvalue() # END for each data set # END for each dry_run mode - diff --git a/gitdb/test/db/test_git.py b/gitdb/test/db/test_git.py index e141c2b..f962067 100644 --- a/gitdb/test/db/test_git.py +++ b/gitdb/test/db/test_git.py @@ -3,7 +3,7 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php from gitdb.test.db.lib import ( - TestDBBase, + TestDBBase, fixture_path, with_rw_directory ) @@ -12,6 +12,7 @@ from gitdb.base import OStream, OInfo from gitdb.util import hex_to_bin, bin_to_hex + class TestGitDB(TestDBBase): def test_reading(self): @@ -28,8 +29,7 @@ def test_reading(self): assert gdb.size() >= ni sha_list = list(gdb.sha_iter()) assert len(sha_list) == gdb.size() - sha_list = sha_list[:ni] # speed up tests ... - + sha_list = sha_list[:ni] # speed up tests ... # This is actually a test for compound functionality, but it doesn't # have a separate test module @@ -39,7 +39,7 @@ def test_reading(self): # mix even/uneven hexshas for i, binsha in enumerate(sha_list): - assert gdb.partial_to_complete_sha_hex(bin_to_hex(binsha)[:8-(i%2)]) == binsha + assert gdb.partial_to_complete_sha_hex(bin_to_hex(binsha)[:8 - (i % 2)]) == binsha # END for each sha self.failUnlessRaises(BadObject, gdb.partial_to_complete_sha_hex, "0000") diff --git a/gitdb/test/db/test_loose.py b/gitdb/test/db/test_loose.py index 1d6af9c..024c194 100644 --- a/gitdb/test/db/test_loose.py +++ b/gitdb/test/db/test_loose.py @@ -3,13 +3,14 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php from gitdb.test.db.lib import ( - TestDBBase, + TestDBBase, with_rw_directory ) from gitdb.db import LooseObjectDB from gitdb.exc import BadObject from gitdb.util import bin_to_hex + class TestLooseDB(TestDBBase): @with_rw_directory diff --git a/gitdb/test/db/test_mem.py b/gitdb/test/db/test_mem.py index 97f7217..eb563c0 100644 --- a/gitdb/test/db/test_mem.py +++ b/gitdb/test/db/test_mem.py @@ -11,6 +11,7 @@ LooseObjectDB ) + class TestMemoryDB(TestDBBase): @with_rw_directory diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index 963a71a..a901581 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -14,6 +14,7 @@ import os import random + class TestPackDB(TestDBBase): @with_rw_directory @@ -53,7 +54,6 @@ def test_writing(self, path): pdb.stream(sha) # END for each sha to query - # test short finding - be a bit more brutal here max_bytes = 19 min_bytes = 2 @@ -61,10 +61,10 @@ def test_writing(self, path): for i, sha in enumerate(sha_list): short_sha = sha[:max((i % max_bytes), min_bytes)] try: - assert pdb.partial_to_complete_sha(short_sha, len(short_sha)*2) == sha + assert pdb.partial_to_complete_sha(short_sha, len(short_sha) * 2) == sha except AmbiguousObjectName: num_ambiguous += 1 - pass # valid, we can have short objects + pass # valid, we can have short objects # END exception handling # END for each sha to find diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index db93082..b774baf 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -3,8 +3,8 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php from gitdb.test.db.lib import ( - TestDBBase, - with_rw_directory, + TestDBBase, + with_rw_directory, fixture_path ) from gitdb.db import ReferenceDB @@ -16,6 +16,7 @@ import os + class TestReferenceDB(TestDBBase): def make_alt_file(self, alt_path, alt_list): diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index d09b1cb..c4acd92 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -24,6 +24,7 @@ #{ Bases class TestBase(unittest.TestCase): + """Base class for all tests""" @@ -49,6 +50,7 @@ def wrapper(self, *args, **kwargs): def with_rw_directory(func): """Create a temporary directory which can be written to, remove it if the test suceeds, but leave it otherwise to aid additional debugging""" + def wrapper(self): path = tempfile.mktemp(prefix=func.__name__) os.mkdir(path) @@ -78,6 +80,7 @@ def wrapper(self): def with_packs_rw(func): """Function that provides a path into which the packs for testing should be copied. Will pass on the path to the actual function afterwards""" + def wrapper(self, path): src_pack_glob = fixture_path('packs/*') copy_files_globbed(src_pack_glob, path, hard_link_ok=True) @@ -91,12 +94,14 @@ def wrapper(self, path): #{ Routines + def fixture_path(relapath=''): """:return: absolute path into the fixture directory :param relapath: relative path into the fixtures directory, or '' to obtain the fixture directory itself""" return os.path.join(os.path.dirname(__file__), 'fixtures', relapath) + def copy_files_globbed(source_glob, target_dir, hard_link_ok=False): """Copy all files found according to the given source glob into the target directory :param hard_link_ok: if True, hard links will be created if possible. Otherwise @@ -127,11 +132,13 @@ def make_bytes(size_in_bytes, randomize=False): a = array('i', producer) return a.tostring() + def make_object(type, data): """:return: bytes resembling an uncompressed object""" odata = "blob %i\0" % len(data) return odata.encode("ascii") + data + def make_memory_file(size_in_bytes, randomize=False): """:return: tuple(size_of_stream, stream) :param randomize: try to produce a very random stream""" @@ -142,24 +149,27 @@ def make_memory_file(size_in_bytes, randomize=False): #{ Stream Utilities + class DummyStream(object): - def __init__(self): - self.was_read = False - self.bytes = 0 - self.closed = False - def read(self, size): - self.was_read = True - self.bytes = size + def __init__(self): + self.was_read = False + self.bytes = 0 + self.closed = False - def close(self): - self.closed = True + def read(self, size): + self.was_read = True + self.bytes = size - def _assert(self): - assert self.was_read + def close(self): + self.closed = True + + def _assert(self): + assert self.was_read class DeriveTest(OStream): + def __init__(self, sha, type, size, stream, *args, **kwargs): self.myarg = kwargs.pop('myarg') self.args = args diff --git a/gitdb/test/performance/__init__.py b/gitdb/test/performance/__init__.py index 8b13789..e69de29 100644 --- a/gitdb/test/performance/__init__.py +++ b/gitdb/test/performance/__init__.py @@ -1 +0,0 @@ - diff --git a/gitdb/test/performance/lib.py b/gitdb/test/performance/lib.py index ec45cf3..cbc52bc 100644 --- a/gitdb/test/performance/lib.py +++ b/gitdb/test/performance/lib.py @@ -13,22 +13,17 @@ #} END invariants - -#{ Base Classes +#{ Base Classes class TestBigRepoR(TestBase): + """TestCase providing access to readonly 'big' repositories using the following member variables: - + * gitrepopath - + * read-only base path of the git source repository, i.e. .../git/.git""" - - #{ Invariants - head_sha_2k = '235d521da60e4699e5bd59ac658b5b48bd76ddca' - head_sha_50 = '32347c375250fd470973a5d76185cac718955fd5' - #} END invariants - + def setUp(self): try: super(TestBigRepoR, self).setUp() @@ -37,11 +32,12 @@ def setUp(self): self.gitrepopath = os.environ.get(k_env_git_repo) if not self.gitrepopath: - logging.info("You can set the %s environment variable to a .git repository of your choice - defaulting to the gitdb repository") + logging.info( + "You can set the %s environment variable to a .git repository of your choice - defaulting to the gitdb repository", k_env_git_repo) ospd = os.path.dirname self.gitrepopath = os.path.join(ospd(ospd(ospd(ospd(__file__)))), '.git') # end assure gitrepo is set assert self.gitrepopath.endswith('.git') - + #} END base classes diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index e460311..bdd2b0a 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -6,7 +6,7 @@ from __future__ import print_function from gitdb.test.performance.lib import ( - TestBigRepoR + TestBigRepoR ) from gitdb import ( @@ -24,19 +24,20 @@ import os from time import time + class TestPackedDBPerformance(TestBigRepoR): - @skip_on_travis_ci + @skip_on_travis_ci def test_pack_random_access(self): pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) - + # sha lookup st = time() sha_list = list(pdb.sha_iter()) elapsed = time() - st ns = len(sha_list) print("PDB: looked up %i shas by index in %f s ( %f shas/s )" % (ns, elapsed, ns / elapsed), file=sys.stderr) - + # sha lookup: best-case and worst case access pdb_pack_info = pdb._pack_info # END shuffle shas @@ -45,13 +46,14 @@ def test_pack_random_access(self): pdb_pack_info(sha) # END for each sha to look up elapsed = time() - st - + # discard cache del(pdb._entities) pdb.entities() - print("PDB: looked up %i sha in %i packs in %f s ( %f shas/s )" % (ns, len(pdb.entities()), elapsed, ns / elapsed), file=sys.stderr) + print("PDB: looked up %i sha in %i packs in %f s ( %f shas/s )" % + (ns, len(pdb.entities()), elapsed, ns / elapsed), file=sys.stderr) # END for each random mode - + # query info and streams only max_items = 10000 # can wait longer when testing memory for pdb_fun in (pdb.info, pdb.stream): @@ -59,9 +61,10 @@ def test_pack_random_access(self): for sha in sha_list[:max_items]: pdb_fun(sha) elapsed = time() - st - print("PDB: Obtained %i object %s by sha in %f s ( %f items/s )" % (max_items, pdb_fun.__name__.upper(), elapsed, max_items / elapsed), file=sys.stderr) + print("PDB: Obtained %i object %s by sha in %f s ( %f items/s )" % + (max_items, pdb_fun.__name__.upper(), elapsed, max_items / elapsed), file=sys.stderr) # END for each function - + # retrieve stream and read all max_items = 5000 pdb_stream = pdb.stream @@ -74,8 +77,9 @@ def test_pack_random_access(self): total_size += stream.size elapsed = time() - st total_kib = total_size / 1000 - print("PDB: Obtained %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % (max_items, total_kib, total_kib/elapsed , elapsed, max_items / elapsed), file=sys.stderr) - + print("PDB: Obtained %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % + (max_items, total_kib, total_kib / elapsed, elapsed, max_items / elapsed), file=sys.stderr) + @skip_on_travis_ci def test_loose_correctness(self): """based on the pack(s) of our packed object DB, we will just copy and verify all objects in the back @@ -89,7 +93,7 @@ def test_loose_correctness(self): mdb = MemoryDB() for c, sha in enumerate(pdb.sha_iter()): ostream = pdb.stream(sha) - # the issue only showed on larger files which are hardly compressible ... + # the issue only showed on larger files which are hardly compressible ... if ostream.type != str_blob_type: continue istream = IStream(ostream.type, ostream.size, ostream.stream) @@ -101,7 +105,7 @@ def test_loose_correctness(self): if c and c % 1000 == 0: print("Verified %i loose object compression/decompression cycles" % c, file=sys.stderr) mdb._cache.clear() - # end for each sha to copy + # end for each sha to copy @skip_on_travis_ci def test_correctness(self): @@ -124,6 +128,6 @@ def test_correctness(self): # END for each index # END for each entity elapsed = time() - st - print("PDB: verified %i objects (crc=%i) in %f s ( %f objects/s )" % (count, crc, elapsed, count / elapsed), file=sys.stderr) + print("PDB: verified %i objects (crc=%i) in %f s ( %f objects/s )" % + (count, crc, elapsed, count / elapsed), file=sys.stderr) # END for each verify mode - diff --git a/gitdb/test/performance/test_pack_streaming.py b/gitdb/test/performance/test_pack_streaming.py index fe160ea..f805e59 100644 --- a/gitdb/test/performance/test_pack_streaming.py +++ b/gitdb/test/performance/test_pack_streaming.py @@ -6,7 +6,7 @@ from __future__ import print_function from gitdb.test.performance.lib import ( - TestBigRepoR + TestBigRepoR ) from gitdb.db.pack import PackedDB @@ -18,27 +18,29 @@ import sys from time import time + class CountedNullStream(NullStream): __slots__ = '_bw' + def __init__(self): self._bw = 0 - + def bytes_written(self): return self._bw - + def write(self, d): self._bw += NullStream.write(self, d) - + class TestPackStreamingPerformance(TestBigRepoR): - + @skip_on_travis_ci def test_pack_writing(self): # see how fast we can write a pack from object streams. # This will not be fast, as we take time for decompressing the streams as well ostream = CountedNullStream() pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) - + ni = 1000 count = 0 st = time() @@ -47,22 +49,23 @@ def test_pack_writing(self): pdb.stream(sha) if count == ni: break - #END gather objects for pack-writing + # END gather objects for pack-writing elapsed = time() - st - print("PDB Streaming: Got %i streams by sha in in %f s ( %f streams/s )" % (ni, elapsed, ni / elapsed), file=sys.stderr) - + print("PDB Streaming: Got %i streams by sha in in %f s ( %f streams/s )" % + (ni, elapsed, ni / elapsed), file=sys.stderr) + st = time() PackEntity.write_pack((pdb.stream(sha) for sha in pdb.sha_iter()), ostream.write, object_count=ni) elapsed = time() - st total_kb = ostream.bytes_written() / 1000 - print(sys.stderr, "PDB Streaming: Wrote pack of size %i kb in %f s (%f kb/s)" % (total_kb, elapsed, total_kb/elapsed), sys.stderr) - - + print(sys.stderr, "PDB Streaming: Wrote pack of size %i kb in %f s (%f kb/s)" % + (total_kb, elapsed, total_kb / elapsed), sys.stderr) + @skip_on_travis_ci def test_stream_reading(self): # raise SkipTest() pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) - + # streaming only, meant for --with-profile runs ni = 5000 count = 0 @@ -78,5 +81,5 @@ def test_stream_reading(self): count += 1 elapsed = time() - st total_kib = total_size / 1000 - print(sys.stderr, "PDB Streaming: Got %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % (ni, total_kib, total_kib/elapsed , elapsed, ni / elapsed), sys.stderr) - + print(sys.stderr, "PDB Streaming: Got %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % + (ni, total_kib, total_kib / elapsed, elapsed, ni / elapsed), sys.stderr) diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index 84c9dea..bd66b26 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -35,22 +35,22 @@ def read_chunked_stream(stream): # END read stream loop assert total == stream.size return stream - - + + #} END utilities class TestObjDBPerformance(TestBigRepoR): - - large_data_size_bytes = 1000*1000*50 # some MiB should do it - moderate_data_size_bytes = 1000*1000*1 # just 1 MiB - - @skip_on_travis_ci + + large_data_size_bytes = 1000 * 1000 * 50 # some MiB should do it + moderate_data_size_bytes = 1000 * 1000 * 1 # just 1 MiB + + @skip_on_travis_ci @with_rw_directory def test_large_data_streaming(self, path): ldb = LooseObjectDB(path) string_ios = list() # list of streams we previously created - - # serial mode + + # serial mode for randomize in range(2): desc = (randomize and 'random ') or '' print("Creating %s data ..." % desc, file=sys.stderr) @@ -59,32 +59,32 @@ def test_large_data_streaming(self, path): elapsed = time() - st print("Done (in %f s)" % elapsed, file=sys.stderr) string_ios.append(stream) - - # writing - due to the compression it will seem faster than it is + + # writing - due to the compression it will seem faster than it is st = time() sha = ldb.store(IStream('blob', size, stream)).binsha elapsed_add = time() - st assert ldb.has_object(sha) db_file = ldb.readable_db_object_path(bin_to_hex(sha)) fsize_kib = os.path.getsize(db_file) / 1000 - - + size_kib = size / 1000 - print("Added %i KiB (filesize = %i KiB) of %s data to loose odb in %f s ( %f Write KiB / s)" % (size_kib, fsize_kib, desc, elapsed_add, size_kib / elapsed_add), file=sys.stderr) - + print("Added %i KiB (filesize = %i KiB) of %s data to loose odb in %f s ( %f Write KiB / s)" % + (size_kib, fsize_kib, desc, elapsed_add, size_kib / elapsed_add), file=sys.stderr) + # reading all at once st = time() ostream = ldb.stream(sha) shadata = ostream.read() elapsed_readall = time() - st - + stream.seek(0) assert shadata == stream.getvalue() - print("Read %i KiB of %s data at once from loose odb in %f s ( %f Read KiB / s)" % (size_kib, desc, elapsed_readall, size_kib / elapsed_readall), file=sys.stderr) - - + print("Read %i KiB of %s data at once from loose odb in %f s ( %f Read KiB / s)" % + (size_kib, desc, elapsed_readall, size_kib / elapsed_readall), file=sys.stderr) + # reading in chunks of 1 MiB - cs = 512*1000 + cs = 512 * 1000 chunks = list() st = time() ostream = ldb.stream(sha) @@ -95,13 +95,14 @@ def test_large_data_streaming(self, path): break # END read in chunks elapsed_readchunks = time() - st - + stream.seek(0) assert b''.join(chunks) == stream.getvalue() - + cs_kib = cs / 1000 - print("Read %i KiB of %s data in %i KiB chunks from loose odb in %f s ( %f Read KiB / s)" % (size_kib, desc, cs_kib, elapsed_readchunks, size_kib / elapsed_readchunks), file=sys.stderr) - + print("Read %i KiB of %s data in %i KiB chunks from loose odb in %f s ( %f Read KiB / s)" % + (size_kib, desc, cs_kib, elapsed_readchunks, size_kib / elapsed_readchunks), file=sys.stderr) + # del db file so we keep something to do os.remove(db_file) # END for each randomization factor diff --git a/gitdb/test/test_base.py b/gitdb/test/test_base.py index 578c29f..519cdfd 100644 --- a/gitdb/test/test_base.py +++ b/gitdb/test/test_base.py @@ -4,10 +4,10 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Test for object db""" from gitdb.test.lib import ( - TestBase, - DummyStream, - DeriveTest, - ) + TestBase, + DummyStream, + DeriveTest, +) from gitdb import ( OInfo, @@ -20,11 +20,11 @@ ) from gitdb.util import ( NULL_BIN_SHA - ) +) from gitdb.typ import ( str_blob_type - ) +) class TestBaseTypes(TestBase): @@ -54,7 +54,6 @@ def test_streams(self): assert dpinfo.delta_info == sha assert dpinfo.pack_offset == 0 - # test ostream stream = DummyStream() ostream = OStream(*(info + (stream, ))) @@ -80,7 +79,7 @@ def test_streams(self): assert stream.bytes == 5 # derive with own args - DeriveTest(sha, str_blob_type, s, stream, 'mine',myarg = 3)._assert() + DeriveTest(sha, str_blob_type, s, stream, 'mine', myarg=3)._assert() # test istream istream = IStream(str_blob_type, s, stream) diff --git a/gitdb/test/test_example.py b/gitdb/test/test_example.py index aa43a09..ed0a885 100644 --- a/gitdb/test/test_example.py +++ b/gitdb/test/test_example.py @@ -4,7 +4,7 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module with examples from the tutorial section of the docs""" from gitdb.test.lib import ( - TestBase, + TestBase, fixture_path ) from gitdb import IStream @@ -12,6 +12,7 @@ from io import BytesIO + class TestExamples(TestBase): def test_base(self): diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 3ab2fec..ff10572 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -43,6 +43,7 @@ def bin_sha_from_filename(filename): return to_bin_sha(os.path.splitext(os.path.basename(filename))[0][5:]) #} END utilities + class TestPack(TestBase): packindexfile_v1 = (fixture_path('packs/pack-c0438c19fb16422b6bbcce24387b3264416d485b.idx'), 1, 67) @@ -50,8 +51,8 @@ class TestPack(TestBase): packindexfile_v2_3_ascii = (fixture_path('packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.idx'), 2, 42) packfile_v2_1 = (fixture_path('packs/pack-c0438c19fb16422b6bbcce24387b3264416d485b.pack'), 2, packindexfile_v1[2]) packfile_v2_2 = (fixture_path('packs/pack-11fdfa9e156ab73caae3b6da867192221f2089c2.pack'), 2, packindexfile_v2[2]) - packfile_v2_3_ascii = (fixture_path('packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.pack'), 2, packindexfile_v2_3_ascii[2]) - + packfile_v2_3_ascii = ( + fixture_path('packs/pack-a2bf8e71d8c18879e499335762dd95119d93d9f1.pack'), 2, packindexfile_v2_3_ascii[2]) def _assert_index_file(self, index, version, size): assert index.packfile_checksum() != index.indexfile_checksum() @@ -74,13 +75,12 @@ def _assert_index_file(self, index, version, size): assert entry[2] == index.crc(oidx) # verify partial sha - for l in (4,8,11,17,20): - assert index.partial_sha_to_index(sha[:l], l*2) == oidx + for l in (4, 8, 11, 17, 20): + assert index.partial_sha_to_index(sha[:l], l * 2) == oidx # END for each object index in indexfile self.failUnlessRaises(ValueError, index.partial_sha_to_index, "\0", 2) - def _assert_pack_file(self, pack, version, size): assert pack.version() == 2 assert pack.size() == size @@ -120,7 +120,6 @@ def _assert_pack_file(self, pack, version, size): dstream.seek(0) assert dstream.read() == data - # read chunks # NOTE: the current implementation is safe, it basically transfers # all calls to the underlying memory map @@ -128,7 +127,6 @@ def _assert_pack_file(self, pack, version, size): # END for each object assert num_obj == size - def test_pack_index(self): # check version 1 and 2 for indexfile, version, size in (self.packindexfile_v1, self.packindexfile_v2): @@ -146,9 +144,9 @@ def test_pack(self): @with_rw_directory def test_pack_entity(self, rw_dir): pack_objs = list() - for packinfo, indexinfo in ( (self.packfile_v2_1, self.packindexfile_v1), - (self.packfile_v2_2, self.packindexfile_v2), - (self.packfile_v2_3_ascii, self.packindexfile_v2_3_ascii)): + for packinfo, indexinfo in ((self.packfile_v2_1, self.packindexfile_v1), + (self.packfile_v2_2, self.packindexfile_v2), + (self.packfile_v2_3_ascii, self.packindexfile_v2_3_ascii)): packfile, version, size = packinfo indexfile, version, size = indexinfo entity = PackEntity(packfile) @@ -193,22 +191,23 @@ def test_pack_entity(self, rw_dir): pack_path = tempfile.mktemp('', "pack", rw_dir) index_path = tempfile.mktemp('', 'index', rw_dir) iteration = 0 + def rewind_streams(): for obj in pack_objs: obj.stream.seek(0) - #END utility - for ppath, ipath, num_obj in zip((pack_path, )*2, (index_path, None), (len(pack_objs), None)): + # END utility + for ppath, ipath, num_obj in zip((pack_path, ) * 2, (index_path, None), (len(pack_objs), None)): pfile = open(ppath, 'wb') iwrite = None if ipath: ifile = open(ipath, 'wb') iwrite = ifile.write - #END handle ip + # END handle ip # make sure we rewind the streams ... we work on the same objects over and over again if iteration > 0: rewind_streams() - #END rewind streams + # END rewind streams iteration += 1 pack_sha, index_sha = PackEntity.write_pack(pack_objs, pfile.write, iwrite, object_count=num_obj) @@ -230,8 +229,8 @@ def rewind_streams(): assert idx.packfile_checksum() == pack_sha assert idx.indexfile_checksum() == index_sha assert idx.size() == len(pack_objs) - #END verify files exist - #END for each packpath, indexpath pair + # END verify files exist + # END for each packpath, indexpath pair # verify the packs throughly rewind_streams() @@ -242,10 +241,9 @@ def rewind_streams(): for use_crc in range(2): assert entity.is_valid_stream(info.binsha, use_crc) # END for each crc mode - #END for each info + # END for each info assert count == len(pack_objs) - def test_pack_64(self): # TODO: hex-edit a pack helping us to verify that we can handle 64 byte offsets # of course without really needing such a huge pack diff --git a/gitdb/test/test_stream.py b/gitdb/test/test_stream.py index 44a557d..9626825 100644 --- a/gitdb/test/test_stream.py +++ b/gitdb/test/test_stream.py @@ -31,10 +31,12 @@ import os from io import BytesIO + class TestStream(TestBase): + """Test stream classes""" - data_sizes = (15, 10000, 1000*1024+512) + data_sizes = (15, 10000, 1000 * 1024 + 512) def _assert_stream_reader(self, stream, cdata, rewind_stream=lambda s: None): """Make stream tests - the orig_stream is seekable, allowing it to be @@ -43,13 +45,13 @@ def _assert_stream_reader(self, stream, cdata, rewind_stream=lambda s: None): :param rewind_stream: function called to rewind the stream to make it ready for reuse""" ns = 10 - assert len(cdata) > ns-1, "Data must be larger than %i, was %i" % (ns, len(cdata)) + assert len(cdata) > ns - 1, "Data must be larger than %i, was %i" % (ns, len(cdata)) # read in small steps ss = len(cdata) // ns for i in range(ns): data = stream.read(ss) - chunk = cdata[i*ss:(i+1)*ss] + chunk = cdata[i * ss:(i + 1) * ss] assert data == chunk # END for each step rest = stream.read() @@ -136,7 +138,7 @@ def test_compressed_writer(self): self.failUnlessRaises(OSError, os.close, fd) # read everything back, compare to data we zip - fd = os.open(path, os.O_RDONLY|getattr(os, 'O_BINARY', 0)) + fd = os.open(path, os.O_RDONLY | getattr(os, 'O_BINARY', 0)) written_data = os.read(fd, os.path.getsize(path)) assert len(written_data) == os.path.getsize(path) os.close(fd) @@ -156,7 +158,7 @@ def test_decompress_reader_special_case(self): data = ostream.read() assert len(data) == ostream.size - # Putting it back in should yield nothing new - after all, we have + # Putting it back in should yield nothing new - after all, we have dump = mdb.store(IStream(ostream.type, ostream.size, BytesIO(data))) assert dump.hexsha == sha # end for each loose object sha to test diff --git a/gitdb/test/test_util.py b/gitdb/test/test_util.py index e79355a..1dee544 100644 --- a/gitdb/test/test_util.py +++ b/gitdb/test/test_util.py @@ -16,6 +16,7 @@ class TestUtils(TestBase): + def test_basics(self): assert to_hex_sha(NULL_HEX_SHA) == NULL_HEX_SHA assert len(to_bin_sha(NULL_HEX_SHA)) == 20 @@ -73,7 +74,6 @@ def test_lockedfd(self): del(lfd) assert not os.path.isfile(lockfilepath) - # write data - concurrently lfd = LockedFD(my_file) olfd = LockedFD(my_file) diff --git a/gitdb/typ.py b/gitdb/typ.py index bc7ba58..98d15f3 100644 --- a/gitdb/typ.py +++ b/gitdb/typ.py @@ -4,7 +4,7 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module containing information about types known to the database""" -str_blob_type = b'blob' +str_blob_type = b'blob' str_commit_type = b'commit' -str_tree_type = b'tree' -str_tag_type = b'tag' +str_tree_type = b'tree' +str_tag_type = b'tag' diff --git a/gitdb/util.py b/gitdb/util.py index 93ba7f0..5b451fa 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -11,10 +11,10 @@ from io import StringIO from smmap import ( - StaticWindowMapManager, - SlidingWindowMapManager, - SlidingWindowMapBuffer - ) + StaticWindowMapManager, + SlidingWindowMapManager, + SlidingWindowMapBuffer +) # initialize our global memory manager instance # Use it to free cached (and unused) resources. @@ -22,7 +22,7 @@ mman = StaticWindowMapManager() else: mman = SlidingWindowMapManager() -#END handle mman +# END handle mman import hashlib @@ -31,6 +31,7 @@ except ImportError: from struct import unpack, calcsize __calcsize_cache = dict() + def unpack_from(fmt, data, offset=0): try: size = __calcsize_cache[fmt] @@ -38,7 +39,7 @@ def unpack_from(fmt, data, offset=0): size = calcsize(fmt) __calcsize_cache[fmt] = size # END exception handling - return unpack(fmt, data[offset : offset + size]) + return unpack(fmt, data[offset: offset + size]) # END own unpack_from implementation @@ -67,8 +68,8 @@ def unpack_from(fmt, data, offset=0): fsync = os.fsync # Backwards compatibility imports -from gitdb.const import ( - NULL_BIN_SHA, +from gitdb.const import ( + NULL_BIN_SHA, NULL_HEX_SHA ) @@ -76,7 +77,9 @@ def unpack_from(fmt, data, offset=0): #{ compatibility stuff ... + class _RandomAccessStringIO(object): + """Wrapper to provide required functionality in case memory maps cannot or may not be used. This is only really required in python 2.4""" __slots__ = '_sio' @@ -96,6 +99,7 @@ def __getitem__(self, i): def __getslice__(self, start, end): return self.getvalue()[start:end] + def byte_ord(b): """ Return the integer representation of the byte string. This supports Python @@ -110,6 +114,7 @@ def byte_ord(b): #{ Routines + def make_sha(source=''.encode("ascii")): """A python2.4 workaround for the sha/hashlib module fiasco @@ -121,6 +126,7 @@ def make_sha(source=''.encode("ascii")): sha1 = sha.sha(source) return sha1 + def allocate_memory(size): """:return: a file-protocol accessible memory block of the given size""" if size == 0: @@ -134,7 +140,7 @@ def allocate_memory(size): # this of course may fail if the amount of memory is not available in # one chunk - would only be the case in python 2.4, being more likely on # 32 bit systems. - return _RandomAccessStringIO("\0"*size) + return _RandomAccessStringIO("\0" * size) # END handle memory allocation @@ -166,6 +172,7 @@ def file_contents_ro(fd, stream=False, allow_mmap=True): return _RandomAccessStringIO(contents) return contents + def file_contents_ro_filepath(filepath, stream=False, allow_mmap=True, flags=0): """Get the file contents at filepath as fast as possible @@ -178,25 +185,28 @@ def file_contents_ro_filepath(filepath, stream=False, allow_mmap=True, flags=0): **Note** for now we don't try to use O_NOATIME directly as the right value needs to be shared per database in fact. It only makes a real difference for loose object databases anyway, and they use it with the help of the ``flags`` parameter""" - fd = os.open(filepath, os.O_RDONLY|getattr(os, 'O_BINARY', 0)|flags) + fd = os.open(filepath, os.O_RDONLY | getattr(os, 'O_BINARY', 0) | flags) try: return file_contents_ro(fd, stream, allow_mmap) finally: close(fd) # END assure file is closed + def sliding_ro_buffer(filepath, flags=0): """ :return: a buffer compatible object which uses our mapped memory manager internally ready to read the whole given filepath""" return SlidingWindowMapBuffer(mman.make_cursor(filepath), flags=flags) + def to_hex_sha(sha): """:return: hexified version of sha""" if len(sha) == 40: return sha return bin_to_hex(sha) + def to_bin_sha(sha): if len(sha) == 20: return sha @@ -209,6 +219,7 @@ def to_bin_sha(sha): #{ Utilities class LazyMixin(object): + """ Base class providing an interface to lazily retrieve attribute values upon first access. If slots are used, memory will only be reserved once the attribute @@ -240,6 +251,7 @@ def _set_cache_(self, attr): class LockedFD(object): + """ This class facilitates a safe read and write operation to a file on disk. If we write to 'file', we obtain a lock file at 'file.lock' and write to @@ -290,7 +302,7 @@ def open(self, write=False, stream=False): # try to open the lock file binary = getattr(os, 'O_BINARY', 0) - lockmode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | binary + lockmode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | binary try: fd = os.open(self._lockfilepath(), lockmode, int("600", 8)) if not write: diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py index a2640fd..c08cab5 100644 --- a/gitdb/utils/compat.py +++ b/gitdb/utils/compat.py @@ -24,7 +24,7 @@ def buffer(obj, offset, size=None): return obj[offset:] else: # return memoryview(obj)[offset:offset+size] - return obj[offset:offset+size] + return obj[offset:offset + size] # end buffer reimplementation memoryview = memoryview diff --git a/gitdb/utils/encoding.py b/gitdb/utils/encoding.py index 2d03ad3..5855062 100644 --- a/gitdb/utils/encoding.py +++ b/gitdb/utils/encoding.py @@ -7,6 +7,7 @@ string_types = (basestring, ) text_type = unicode + def force_bytes(data, encoding="ascii"): if isinstance(data, bytes): return data @@ -16,6 +17,7 @@ def force_bytes(data, encoding="ascii"): return data + def force_text(data, encoding="utf-8"): if isinstance(data, text_type): return data diff --git a/setup.py b/setup.py index 4f8d1d5..6c67dc6 100755 --- a/setup.py +++ b/setup.py @@ -1,70 +1,74 @@ #!/usr/bin/env python -from distutils.core import setup, Extension +from distutils.core import setup, Extension from distutils.command.build_py import build_py from distutils.command.build_ext import build_ext -import os, sys +import os +import sys -# wow, this is a mixed bag ... I am pretty upset about all of this ... +# wow, this is a mixed bag ... I am pretty upset about all of this ... setuptools_build_py_module = None try: - # don't pull it in if we don't have to - if 'setuptools' in sys.modules: - import setuptools.command.build_py as setuptools_build_py_module - from setuptools.command.build_ext import build_ext + # don't pull it in if we don't have to + if 'setuptools' in sys.modules: + import setuptools.command.build_py as setuptools_build_py_module + from setuptools.command.build_ext import build_ext except ImportError: - pass + pass + class build_ext_nofail(build_ext): - """Doesn't fail when build our optional extensions""" - def run(self): - try: - build_ext.run(self) - except Exception: - print("Ignored failure when building extensions, pure python modules will be used instead") - # END ignore errors - + + """Doesn't fail when build our optional extensions""" + + def run(self): + try: + build_ext.run(self) + except Exception: + print("Ignored failure when building extensions, pure python modules will be used instead") + # END ignore errors + def get_data_files(self): - """Can you feel the pain ? So, in python2.5 and python2.4 coming with maya, - the line dealing with the ``plen`` has a bug which causes it to truncate too much. - It is fixed in the system interpreters as they receive patches, and shows how - bad it is if something doesn't have proper unittests. - The code here is a plain copy of the python2.6 version which works for all. - - Generate list of '(package,src_dir,build_dir,filenames)' tuples""" - data = [] - if not self.packages: - return data - - # this one is just for the setup tools ! They don't iniitlialize this variable - # when they should, but do it on demand using this method.Its crazy - if hasattr(self, 'analyze_manifest'): - self.analyze_manifest() - # END handle setuptools ... - - for package in self.packages: - # Locate package source directory - src_dir = self.get_package_dir(package) - - # Compute package build directory - build_dir = os.path.join(*([self.build_lib] + package.split('.'))) - - # Length of path to strip from found files - plen = 0 - if src_dir: - plen = len(src_dir)+1 - - # Strip directory from globbed filenames - filenames = [ - file[plen:] for file in self.find_data_files(package, src_dir) - ] - data.append((package, src_dir, build_dir, filenames)) - return data - + """Can you feel the pain ? So, in python2.5 and python2.4 coming with maya, + the line dealing with the ``plen`` has a bug which causes it to truncate too much. + It is fixed in the system interpreters as they receive patches, and shows how + bad it is if something doesn't have proper unittests. + The code here is a plain copy of the python2.6 version which works for all. + + Generate list of '(package,src_dir,build_dir,filenames)' tuples""" + data = [] + if not self.packages: + return data + + # this one is just for the setup tools ! They don't iniitlialize this variable + # when they should, but do it on demand using this method.Its crazy + if hasattr(self, 'analyze_manifest'): + self.analyze_manifest() + # END handle setuptools ... + + for package in self.packages: + # Locate package source directory + src_dir = self.get_package_dir(package) + + # Compute package build directory + build_dir = os.path.join(*([self.build_lib] + package.split('.'))) + + # Length of path to strip from found files + plen = 0 + if src_dir: + plen = len(src_dir) + 1 + + # Strip directory from globbed filenames + filenames = [ + file[plen:] for file in self.find_data_files(package, src_dir) + ] + data.append((package, src_dir, build_dir, filenames)) + return data + build_py.get_data_files = get_data_files if setuptools_build_py_module: - setuptools_build_py_module.build_py._get_data_files = get_data_files + setuptools_build_py_module.build_py._get_data_files = get_data_files # END apply setuptools patch too # NOTE: This is currently duplicated from the gitdb.__init__ module, as we cannot @@ -76,15 +80,15 @@ def get_data_files(self): version_info = (0, 6, 1) __version__ = '.'.join(str(i) for i in version_info) -setup(cmdclass={'build_ext':build_ext_nofail}, - name = "gitdb", - version = __version__, - description = "Git Object Database", - author = __author__, - author_email = __contact__, - url = __homepage__, - packages = ('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), - package_dir = {'gitdb':'gitdb'}, +setup(cmdclass={'build_ext': build_ext_nofail}, + name="gitdb", + version=__version__, + description="Git Object Database", + author=__author__, + author_email=__contact__, + url=__homepage__, + packages=('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), + package_dir = {'gitdb': 'gitdb'}, ext_modules=[Extension('gitdb._perf', ['gitdb/_fun.c', 'gitdb/_delta_apply.c'], include_dirs=['gitdb'])], license = "BSD License", zip_safe=False, @@ -94,26 +98,26 @@ def get_data_files(self): # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # Picked from - # http://pypi.python.org/pypi?:action=list_classifiers - #"Development Status :: 1 - Planning", - #"Development Status :: 2 - Pre-Alpha", - #"Development Status :: 3 - Alpha", - # "Development Status :: 4 - Beta", - "Development Status :: 5 - Production/Stable", - #"Development Status :: 6 - Mature", - #"Development Status :: 7 - Inactive", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS :: MacOS X", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - ],) + # http://pypi.python.org/pypi?:action=list_classifiers + #"Development Status :: 1 - Planning", + #"Development Status :: 2 - Pre-Alpha", + #"Development Status :: 3 - Alpha", + # "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", + #"Development Status :: 6 - Mature", + #"Development Status :: 7 - Inactive", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", +],) From b1c9d3eb5b13f2feecb242701f5b4842184f6234 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jan 2015 15:28:56 +0100 Subject: [PATCH 009/158] A minor fix after porting git-python over to PY3 It doesn't do anything (in terms of fixing an issue), but it should be more correct than what was there previously --- gitdb/utils/encoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitdb/utils/encoding.py b/gitdb/utils/encoding.py index 5855062..4d270af 100644 --- a/gitdb/utils/encoding.py +++ b/gitdb/utils/encoding.py @@ -22,7 +22,7 @@ def force_text(data, encoding="utf-8"): if isinstance(data, text_type): return data - if isinstance(data, string_types): + if isinstance(data, bytes): return data.decode(encoding) if compat.PY3: From fdc1d68b01f0d5dd601cdcc29df0eee19787d7c9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jan 2015 17:02:35 +0100 Subject: [PATCH 010/158] Fixed python 3 compatibility issue that only showed on windows And bumped version to 0.6.2 --- gitdb/__init__.py | 2 +- gitdb/util.py | 12 ++++++------ setup.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 791a2ef..020a579 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 1) +version_info = (0, 6, 2) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/util.py b/gitdb/util.py index 5b451fa..8f80156 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -8,7 +8,7 @@ import sys import errno -from io import StringIO +from io import BytesIO from smmap import ( StaticWindowMapManager, @@ -78,14 +78,14 @@ def unpack_from(fmt, data, offset=0): #{ compatibility stuff ... -class _RandomAccessStringIO(object): +class _RandomAccessBytesIO(object): """Wrapper to provide required functionality in case memory maps cannot or may not be used. This is only really required in python 2.4""" __slots__ = '_sio' def __init__(self, buf=''): - self._sio = StringIO(buf) + self._sio = BytesIO(buf) def __getattr__(self, attr): return getattr(self._sio, attr) @@ -130,7 +130,7 @@ def make_sha(source=''.encode("ascii")): def allocate_memory(size): """:return: a file-protocol accessible memory block of the given size""" if size == 0: - return _RandomAccessStringIO('') + return _RandomAccessBytesIO(b'') # END handle empty chunks gracefully try: @@ -140,7 +140,7 @@ def allocate_memory(size): # this of course may fail if the amount of memory is not available in # one chunk - would only be the case in python 2.4, being more likely on # 32 bit systems. - return _RandomAccessStringIO("\0" * size) + return _RandomAccessBytesIO(b"\0" * size) # END handle memory allocation @@ -169,7 +169,7 @@ def file_contents_ro(fd, stream=False, allow_mmap=True): # read manully contents = os.read(fd, os.fstat(fd).st_size) if stream: - return _RandomAccessStringIO(contents) + return _RandomAccessBytesIO(contents) return contents diff --git a/setup.py b/setup.py index 6c67dc6..12c936a 100755 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ def get_data_files(self): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 1) +version_info = (0, 6, 2) __version__ = '.'.join(str(i) for i in version_info) setup(cmdclass={'build_ext': build_ext_nofail}, @@ -92,8 +92,8 @@ def get_data_files(self): ext_modules=[Extension('gitdb._perf', ['gitdb/_fun.c', 'gitdb/_delta_apply.c'], include_dirs=['gitdb'])], license = "BSD License", zip_safe=False, - requires=('smmap (>=0.8.3)', ), - install_requires=('smmap >= 0.8.3'), + requires=('smmap (>=0.8.5)', ), + install_requires=('smmap >= 0.8.5'), long_description = """GitDB is a pure-Python git object database""", # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ From cb72f81e1407a86d85215a7fba4c2905c2451e0c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jan 2015 18:17:23 +0100 Subject: [PATCH 011/158] Added coverage configuration Adjusted sublime project too --- .coveragerc | 11 +++++++++++ .gitignore | 3 ++- .travis.yml | 2 +- etc/sublime-text/gitdb.sublime-project | 24 +++++++----------------- 4 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..45d72ab --- /dev/null +++ b/.coveragerc @@ -0,0 +1,11 @@ +[run] +source = gitdb + +; to make nosetests happy +[report] +omit = + */smmap/* + */yaml* + */tests/* + */python?.?/* + */site-packages/nose/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index c6247db..e0b4e85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ MANIFEST +.coverage build/ dist/ *.pyc *.o *.so .noseids -*.sublime-workspace \ No newline at end of file +*.sublime-workspace diff --git a/.travis.yml b/.travis.yml index 761edc1..d436229 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ git: install: - pip install coveralls script: - - nosetests -v + - nosetests -v --with-coverage after_success: - coveralls diff --git a/etc/sublime-text/gitdb.sublime-project b/etc/sublime-text/gitdb.sublime-project index bc0e37f..d0e2e51 100644 --- a/etc/sublime-text/gitdb.sublime-project +++ b/etc/sublime-text/gitdb.sublime-project @@ -15,7 +15,10 @@ "folder_exclude_patterns" : [ ".git", "cover", - "gitdb/ext" + "gitdb/ext", + "dist", + "doc/build", + ".tox" ] }, // SMMAP @@ -32,22 +35,9 @@ "folder_exclude_patterns" : [ ".git", "cover", - ] - }, - // ASYNC - //////// - { - "follow_symlinks": true, - "path": "../../gitdb/ext/async", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", + "dist", + "doc/build", + ".tox" ] }, ] From de96c522ff20fa99d13128784a393b619dd0b33b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jan 2015 18:26:18 +0100 Subject: [PATCH 012/158] Fixed yet another issue with smmap's latest changes Now we deal with memory views as well ... --- gitdb/pack.py | 9 +++++++-- gitdb/utils/compat.py | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/gitdb/pack.py b/gitdb/pack.py index b4ba787..d2666d6 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -62,7 +62,12 @@ from binascii import crc32 from gitdb.const import NULL_BYTE -from gitdb.utils.compat import izip, buffer, xrange +from gitdb.utils.compat import ( + izip, + buffer, + xrange, + to_bytes +) import tempfile import array @@ -864,7 +869,7 @@ def collect_streams_at_offset(self, offset): stream = streams[-1] while stream.type_id in delta_types: if stream.type_id == REF_DELTA: - sindex = self._index.sha_to_index(stream.delta_info) + sindex = self._index.sha_to_index(to_bytes(stream.delta_info)) if sindex is None: break stream = self._pack.stream(self._index.offset(sindex)) diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py index c08cab5..a7899cb 100644 --- a/gitdb/utils/compat.py +++ b/gitdb/utils/compat.py @@ -15,6 +15,9 @@ # Python 2 buffer = buffer memoryview = buffer + # Assume no memory view ... + def to_bytes(i): + return i except NameError: # Python 3 has no `buffer`; only `memoryview` # However, it's faster to just slice the object directly, maybe it keeps a view internally @@ -26,6 +29,11 @@ def buffer(obj, offset, size=None): # return memoryview(obj)[offset:offset+size] return obj[offset:offset + size] # end buffer reimplementation + # smmap can return memory view objects, which can't be compared as buffers/bytes can ... + def to_bytes(i): + if isinstance(i, memoryview): + return i.tobytes() + return i memoryview = memoryview From a64e64a9735e8067ca196ab19711d136bd9b309c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jan 2015 18:27:22 +0100 Subject: [PATCH 013/158] Bumped version to 0.6.3 --- gitdb/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 020a579..2ba8725 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 2) +version_info = (0, 6, 3) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index 12c936a..e634e37 100755 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ def get_data_files(self): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 2) +version_info = (0, 6, 3) __version__ = '.'.join(str(i) for i in version_info) setup(cmdclass={'build_ext': build_ext_nofail}, From 9b3a34b7d00285cf9028d19e82de6b155d0096c7 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 6 Jan 2015 18:53:52 +0100 Subject: [PATCH 014/158] Improved coverage configuration --- .coveragerc | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index 45d72ab..71b6ef7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,9 +3,5 @@ source = gitdb ; to make nosetests happy [report] -omit = - */smmap/* - */yaml* - */tests/* - */python?.?/* - */site-packages/nose/* \ No newline at end of file +include = */gitdb/* +omit = */gitdb/ext/* From 560a211001064261eb25ca874980591790fb7986 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 7 Jan 2015 18:09:45 +0100 Subject: [PATCH 015/158] Fixed possible file-handle leak Configured travis to artificially restrict handle count to protect from regression in that regard --- .travis.yml | 2 ++ gitdb/stream.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d436229..8cab822 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,8 @@ git: install: - pip install coveralls script: + - ulimit -n 48 + - ulimit -n - nosetests -v --with-coverage after_success: - coveralls diff --git a/gitdb/stream.py b/gitdb/stream.py index 4478a0f..d855257 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -91,9 +91,7 @@ def _set_cache_(self, attr): self._parse_header_info() def __del__(self): - if self._close: - self._m.close() - # END handle resource freeing + self.close() def _parse_header_info(self): """If this stream contains object data, parse the header info and skip the @@ -141,6 +139,16 @@ def data(self): """:return: random access compatible data we are working on""" return self._m + def close(self): + """Close our underlying stream of compressed bytes if this was allowed during initialization + :return: True if we closed the underlying stream + :note: can be called safely + """ + if self._close: + self._m.close() + self._close = False + # END handle resource freeing + def compressed_bytes_read(self): """ :return: number of compressed bytes read. This includes the bytes it From be294278a0087f21d565a1084fb220ff936ae0bd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Wed, 7 Jan 2015 19:57:19 +0100 Subject: [PATCH 016/158] Protected stream closure against possibilty of being a bytes For some reason, it gets bytes where it did expect a stream ... . Probably I should have figured out where this was input, instead of fixing it the brutal way --- gitdb/db/loose.py | 3 ++- gitdb/stream.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index e924080..4732b56 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -159,7 +159,8 @@ def info(self, sha): typ, size = loose_object_header_info(m) return OInfo(sha, typ, size) finally: - m.close() + if hasattr(m, 'close'): + m.close() # END assure release of system resources def stream(self, sha): diff --git a/gitdb/stream.py b/gitdb/stream.py index d855257..826def3 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -145,7 +145,8 @@ def close(self): :note: can be called safely """ if self._close: - self._m.close() + if hasattr(self._m, 'close'): + self._m.close() self._close = False # END handle resource freeing From 7bde7b098b07291227fcbc4eb900ebf13c9191a2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 8 Jan 2015 17:34:53 +0100 Subject: [PATCH 017/158] Fixed up tests to use the GITDB_TEST_GIT_REPO_BASE at all times I have verified that all tests are working, even without a parent git repository, as long as the said environment variable is set. Fixes #16 --- gitdb/exc.py | 12 +++++++----- gitdb/test/db/test_git.py | 3 ++- gitdb/test/db/test_ref.py | 2 +- gitdb/test/lib.py | 29 ++++++++++++++++++++++++++++- gitdb/test/performance/lib.py | 30 ++---------------------------- gitdb/test/test_example.py | 8 +++----- 6 files changed, 43 insertions(+), 41 deletions(-) diff --git a/gitdb/exc.py b/gitdb/exc.py index d58442f..817ac7b 100644 --- a/gitdb/exc.py +++ b/gitdb/exc.py @@ -12,12 +12,10 @@ class ODBError(Exception): class InvalidDBRoot(ODBError): - """Thrown if an object database cannot be initialized at the given path""" class BadObject(ODBError): - """The object with the given SHA does not exist. Instantiate with the failed sha""" @@ -25,19 +23,23 @@ def __str__(self): return "BadObject: %s" % to_hex_sha(self.args[0]) -class ParseError(ODBError): +class BadName(ODBError): + """A name provided to rev_parse wasn't understood""" + + def __str__(self): + return "Ref '%s' did not resolve to an object" % self.args[0] + +class ParseError(ODBError): """Thrown if the parsing of a file failed due to an invalid format""" class AmbiguousObjectName(ODBError): - """Thrown if a possibly shortened name does not uniquely represent a single object in the database""" class BadObjectType(ODBError): - """The object had an unsupported type""" diff --git a/gitdb/test/db/test_git.py b/gitdb/test/db/test_git.py index f962067..f28ffb7 100644 --- a/gitdb/test/db/test_git.py +++ b/gitdb/test/db/test_git.py @@ -2,6 +2,7 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php +import os from gitdb.test.db.lib import ( TestDBBase, fixture_path, @@ -16,7 +17,7 @@ class TestGitDB(TestDBBase): def test_reading(self): - gdb = GitDB(fixture_path('../../../.git/objects')) + gdb = GitDB(os.path.join(self.gitrepopath, 'objects')) # we have packs and loose objects, alternates doesn't necessarily exist assert 1 < len(gdb.databases()) < 4 diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index b774baf..25cf37d 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -40,7 +40,7 @@ def test_writing(self, path): # setup alternate file # add two, one is invalid - own_repo_path = fixture_path('../../../.git/objects') # use own repo + own_repo_path = os.path.join(self.gitrepopath, 'objects') # use own repo self.make_alt_file(alt_path, [own_repo_path, "invalid/path"]) rdb.update_cache() assert len(rdb.databases()) == 1 diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index c4acd92..a089eac 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -18,14 +18,41 @@ import shutil import os import gc +import logging from functools import wraps #{ Bases class TestBase(unittest.TestCase): + """Base class for all tests - """Base class for all tests""" + TestCase providing access to readonly repositories using the following member variables. + + * gitrepopath + + * read-only base path of the git source repository, i.e. .../git/.git + """ + + #{ Invvariants + k_env_git_repo = "GITDB_TEST_GIT_REPO_BASE" + #} END invariants + + @classmethod + def setUpClass(cls): + try: + super(TestBase, cls).setUpClass() + except AttributeError: + pass + + cls.gitrepopath = os.environ.get(cls.k_env_git_repo) + if not cls.gitrepopath: + logging.info( + "You can set the %s environment variable to a .git repository of your choice - defaulting to the gitdb repository", cls.k_env_git_repo) + ospd = os.path.dirname + cls.gitrepopath = os.path.join(ospd(ospd(ospd(__file__))), '.git') + # end assure gitrepo is set + assert cls.gitrepopath.endswith('.git') #} END bases diff --git a/gitdb/test/performance/lib.py b/gitdb/test/performance/lib.py index cbc52bc..fa4dd20 100644 --- a/gitdb/test/performance/lib.py +++ b/gitdb/test/performance/lib.py @@ -3,41 +3,15 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Contains library functions""" -import os -import logging from gitdb.test.lib import TestBase -#{ Invvariants -k_env_git_repo = "GITDB_TEST_GIT_REPO_BASE" -#} END invariants - #{ Base Classes class TestBigRepoR(TestBase): - - """TestCase providing access to readonly 'big' repositories using the following - member variables: - - * gitrepopath - - * read-only base path of the git source repository, i.e. .../git/.git""" - - def setUp(self): - try: - super(TestBigRepoR, self).setUp() - except AttributeError: - pass - - self.gitrepopath = os.environ.get(k_env_git_repo) - if not self.gitrepopath: - logging.info( - "You can set the %s environment variable to a .git repository of your choice - defaulting to the gitdb repository", k_env_git_repo) - ospd = os.path.dirname - self.gitrepopath = os.path.join(ospd(ospd(ospd(ospd(__file__)))), '.git') - # end assure gitrepo is set - assert self.gitrepopath.endswith('.git') + """A placeholder in case we want to add additional functionality to all performance test-cases + """ #} END base classes diff --git a/gitdb/test/test_example.py b/gitdb/test/test_example.py index ed0a885..6e80bf5 100644 --- a/gitdb/test/test_example.py +++ b/gitdb/test/test_example.py @@ -3,10 +3,8 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Module with examples from the tutorial section of the docs""" -from gitdb.test.lib import ( - TestBase, - fixture_path -) +import os +from gitdb.test.lib import TestBase from gitdb import IStream from gitdb.db import LooseObjectDB @@ -16,7 +14,7 @@ class TestExamples(TestBase): def test_base(self): - ldb = LooseObjectDB(fixture_path("../../../.git/objects")) + ldb = LooseObjectDB(os.path.join(self.gitrepopath, 'objects')) for sha1 in ldb.sha_iter(): oinfo = ldb.info(sha1) From f2233fbf40f3f69309ce5cc714e99fcbdcd33ec3 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 8 Jan 2015 17:49:05 +0100 Subject: [PATCH 018/158] Removed unused imports - should have been in the last commit obviously --- gitdb/test/db/test_git.py | 1 - gitdb/test/db/test_ref.py | 1 - 2 files changed, 2 deletions(-) diff --git a/gitdb/test/db/test_git.py b/gitdb/test/db/test_git.py index f28ffb7..2bda18f 100644 --- a/gitdb/test/db/test_git.py +++ b/gitdb/test/db/test_git.py @@ -5,7 +5,6 @@ import os from gitdb.test.db.lib import ( TestDBBase, - fixture_path, with_rw_directory ) from gitdb.exc import BadObject diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index 25cf37d..0e90f93 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -5,7 +5,6 @@ from gitdb.test.db.lib import ( TestDBBase, with_rw_directory, - fixture_path ) from gitdb.db import ReferenceDB From a88a777df3909a61be97f1a7b1194dad6de25702 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 8 Jan 2015 18:25:46 +0100 Subject: [PATCH 019/158] Make tests independent of actual repository data Therefore, hardcoded sha's are not allowed anymore, as the contents of the repository is unknown. Fixes #16, for real this time ;) --- gitdb/test/db/test_git.py | 7 ++++--- gitdb/test/db/test_ref.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/gitdb/test/db/test_git.py b/gitdb/test/db/test_git.py index 2bda18f..acc0f15 100644 --- a/gitdb/test/db/test_git.py +++ b/gitdb/test/db/test_git.py @@ -10,7 +10,7 @@ from gitdb.exc import BadObject from gitdb.db import GitDB from gitdb.base import OStream, OInfo -from gitdb.util import hex_to_bin, bin_to_hex +from gitdb.util import bin_to_hex class TestGitDB(TestDBBase): @@ -22,7 +22,7 @@ def test_reading(self): assert 1 < len(gdb.databases()) < 4 # access should be possible - gitdb_sha = hex_to_bin("5690fd0d3304f378754b23b098bd7cb5f4aa1976") + gitdb_sha = next(gdb.sha_iter()) assert isinstance(gdb.info(gitdb_sha), OInfo) assert isinstance(gdb.stream(gitdb_sha), OStream) ni = 50 @@ -35,7 +35,8 @@ def test_reading(self): # have a separate test module # test partial shas # this one as uneven and quite short - assert gdb.partial_to_complete_sha_hex('155b6') == hex_to_bin("155b62a9af0aa7677078331e111d0f7aa6eb4afc") + gitdb_sha_hex = bin_to_hex(gitdb_sha) + assert gdb.partial_to_complete_sha_hex(gitdb_sha_hex[:5]) == gitdb_sha # mix even/uneven hexshas for i, binsha in enumerate(sha_list): diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index 0e90f93..6bac245 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -45,7 +45,7 @@ def test_writing(self, path): assert len(rdb.databases()) == 1 # we should now find a default revision of ours - gitdb_sha = hex_to_bin("5690fd0d3304f378754b23b098bd7cb5f4aa1976") + gitdb_sha = next(rdb.sha_iter()) assert rdb.has_object(gitdb_sha) # remove valid From 13ad9b1199331a35e23f65c735acf482be09eae3 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Thu, 8 Jan 2015 13:10:28 -0500 Subject: [PATCH 020/158] minor spell fixes + empty line unification + comparison for python 2.6 --- gitdb/base.py | 6 +++--- gitdb/exc.py | 2 -- gitdb/stream.py | 6 +++--- gitdb/util.py | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/gitdb/base.py b/gitdb/base.py index 5760b8a..42e71d0 100644 --- a/gitdb/base.py +++ b/gitdb/base.py @@ -19,7 +19,7 @@ class OInfo(tuple): - """Carries information about an object in an ODB, provding information + """Carries information about an object in an ODB, providing information about the binary sha of the object, the type_string as well as the uncompressed size in bytes. @@ -29,7 +29,7 @@ class OInfo(tuple): assert dbi[1] == dbi.type assert dbi[2] == dbi.size - The type is designed to be as lighteight as possible.""" + The type is designed to be as lightweight as possible.""" __slots__ = tuple() def __new__(cls, sha, type, size): @@ -69,7 +69,7 @@ class OPackInfo(tuple): does not include a sha. Additionally, the pack_offset is the absolute offset into the packfile at which - all object information is located. The data_offset property points to the abosolute + all object information is located. The data_offset property points to the absolute location in the pack at which that actual data stream can be found.""" __slots__ = tuple() diff --git a/gitdb/exc.py b/gitdb/exc.py index 817ac7b..947e5d8 100644 --- a/gitdb/exc.py +++ b/gitdb/exc.py @@ -7,7 +7,6 @@ class ODBError(Exception): - """All errors thrown by the object database""" @@ -44,5 +43,4 @@ class BadObjectType(ODBError): class UnsupportedOperation(ODBError): - """Thrown if the given operation cannot be supported by the object database""" diff --git a/gitdb/stream.py b/gitdb/stream.py index 826def3..aaf5820 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -62,7 +62,7 @@ class DecompressMemMapReader(LazyMixin): hence we try to find a good tradeoff between allocation time and number of times we actually allocate. An own zlib implementation would be good here to better support streamed reading - it would only need to keep the mmap - and decompress it into chunks, thats all ... """ + and decompress it into chunks, that's all ... """ __slots__ = ('_m', '_zip', '_buf', '_buflen', '_br', '_cws', '_cwe', '_s', '_close', '_cbr', '_phi') @@ -128,7 +128,7 @@ def new(self, m, close_on_deletion=False): This method parses the object header from m and returns the parsed type and size, as well as the created stream instance. - :param m: memory map on which to oparate. It must be object data ( header + contents ) + :param m: memory map on which to operate. It must be object data ( header + contents ) :param close_on_deletion: if True, the memory map will be closed once we are being deleted""" inst = DecompressMemMapReader(m, close_on_deletion, 0) @@ -175,7 +175,7 @@ def compressed_bytes_read(self): # Only scrub the stream forward if we are officially done with the # bytes we were to have. if self._br == self._s and not self._zip.unused_data: - # manipulate the bytes-read to allow our own read method to coninute + # manipulate the bytes-read to allow our own read method to continue # but keep the window at its current position self._br = 0 if hasattr(self._zip, 'status'): diff --git a/gitdb/util.py b/gitdb/util.py index 8f80156..242be44 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -18,7 +18,7 @@ # initialize our global memory manager instance # Use it to free cached (and unused) resources. -if sys.version_info[1] < 6: +if sys.version_info < (2, 6): mman = StaticWindowMapManager() else: mman = SlidingWindowMapManager() From 5b0dc5f89a666f450f39ef0002cf6d1761ecfca8 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Jan 2015 19:10:42 +0100 Subject: [PATCH 021/158] Adjusted stream logic to make it work on all tested platforms ... . As taken from https://github.com/gitpython-developers/gitdb/blob/master/gitdb/stream.py#L292 -> NOTE: Behavior changed in PY2.7 onward, which requires special handling to make the tests work properly. They are thorough, and I assume it is truly working. Why is this logic as convoluted as it is ? Please look at the table in https://github.com/gitpython-developers/gitdb/issues/19 to learn about the test-results. Bascially, on py2.6, you want to use branch 1, whereas on all other python version, the second branch will be the one that works. However, the zlib VERSIONs as well as the platform check is used to further match the entries in the table in the github issue. This is it ... it was the only way I could make this work everywhere. IT's CERTAINLY GOING TO BITE US IN THE FUTURE ... . <- Fixes #19 --- gitdb/fun.py | 2 +- gitdb/pack.py | 1 + gitdb/stream.py | 12 +++++++++--- setup.py | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/gitdb/fun.py b/gitdb/fun.py index 17da4e5..ac9d993 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -426,7 +426,7 @@ def pack_object_header_info(data): s = 4 # starting bit-shift size if PY3: while c & 0x80: - c = data[i] + c = byte_ord(data[i]) i += 1 size += (c & 0x7f) << s s += 7 diff --git a/gitdb/pack.py b/gitdb/pack.py index d2666d6..511e557 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -552,6 +552,7 @@ def _iter_objects(self, start_offset, as_stream=True): # the amount of compressed bytes we need to get to the next offset stream_copy(ostream.read, null.write, ostream.size, chunk_size) + assert ostream.stream._br == ostream.size cur_offset += (data_offset - ostream.pack_offset) + ostream.stream.compressed_bytes_read() # if a stream is requested, reset it beforehand diff --git a/gitdb/stream.py b/gitdb/stream.py index aaf5820..04dd79f 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -289,11 +289,18 @@ def read(self, size=-1): # if we hit the end of the stream # NOTE: Behavior changed in PY2.7 onward, which requires special handling to make the tests work properly. # They are thorough, and I assume it is truly working. - if PY26: + # Why is this logic as convoluted as it is ? Please look at the table in + # https://github.com/gitpython-developers/gitdb/issues/19 to learn about the test-results. + # Bascially, on py2.6, you want to use branch 1, whereas on all other python version, the second branch + # will be the one that works. + # However, the zlib VERSIONs as well as the platform check is used to further match the entries in the + # table in the github issue. This is it ... it was the only way I could make this work everywhere. + # IT's CERTAINLY GOING TO BITE US IN THE FUTURE ... . + if PY26 or ((zlib.ZLIB_VERSION == '1.2.7' or zlib.ZLIB_VERSION == '1.2.5') and not sys.platform == 'darwin'): unused_datalen = len(self._zip.unconsumed_tail) else: unused_datalen = len(self._zip.unconsumed_tail) + len(self._zip.unused_data) - # end handle very special case ... + # # end handle very special case ... self._cbr += len(indata) - unused_datalen self._br += len(dcompdat) @@ -374,7 +381,6 @@ def _set_cache_too_slow_without_c(self, attr): # Aggregate all deltas into one delta in reverse order. Hence we take # the last delta, and reverse-merge its ancestor delta, until we receive # the final delta data stream. - # print "Handling %i delta streams, sizes: %s" % (len(self._dstreams), [ds.size for ds in self._dstreams]) dcl = connect_deltas(self._dstreams) # call len directly, as the (optional) c version doesn't implement the sequence diff --git a/setup.py b/setup.py index e634e37..be2f6e2 100755 --- a/setup.py +++ b/setup.py @@ -118,6 +118,7 @@ def get_data_files(self): "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", ],) From b3237e804ae313503f5479349f90066c356b1548 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 12 Jan 2015 20:38:38 +0100 Subject: [PATCH 022/158] Bumped version to 0.6.4 --- gitdb/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 2ba8725..6554cf9 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 3) +version_info = (0, 6, 4) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index be2f6e2..e4b1b1d 100755 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ def get_data_files(self): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 3) +version_info = (0, 6, 4) __version__ = '.'.join(str(i) for i in version_info) setup(cmdclass={'build_ext': build_ext_nofail}, From 9aae93ea584c8cf9d1539a60e41c5c37119401d6 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 22 Jan 2015 18:16:34 +0100 Subject: [PATCH 023/158] Added issuestats to readme file --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 186218d..766d9d6 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,12 @@ DEVELOPMENT .. image:: https://coveralls.io/repos/gitpython-developers/gitdb/badge.png :target: https://coveralls.io/r/gitpython-developers/gitdb +.. image:: http://www.issuestats.com/github/gitpython-developers/gitdb/badge/pr + :target: http://www.issuestats.com/github/gitpython-developers/gitdb + +.. image:: http://www.issuestats.com/github/gitpython-developers/gitdb/badge/issue + :target: http://www.issuestats.com/github/gitpython-developers/gitdb + The library is considered mature, and not under active development. It's primary (known) use is in git-python. INFRASTRUCTURE From 016b58f3a7638d59ee766433649253e2d53e18b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20M=2E=20Bravo?= Date: Tue, 7 Apr 2015 08:32:38 -0500 Subject: [PATCH 024/158] Duplicate `const` fixed Remove duplicate `const` to stop the warning: "duplicate 'const' declaration specifier" --- gitdb/_delta_apply.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gitdb/_delta_apply.c b/gitdb/_delta_apply.c index 8b0f8e0..f4fffdc 100644 --- a/gitdb/_delta_apply.c +++ b/gitdb/_delta_apply.c @@ -413,7 +413,7 @@ uint DIV_info_rbound(const DeltaInfoVector* vec, const DeltaInfo* di) // return size of the given delta info item inline -uint DIV_info_size2(const DeltaInfoVector* vec, const DeltaInfo* di, const DeltaInfo const* veclast) +uint DIV_info_size2(const DeltaInfoVector* vec, const DeltaInfo* di, const DeltaInfo* const veclast) { if (veclast == di){ return vec->di_last_size; @@ -534,7 +534,7 @@ uint DIV_count_slice_bytes(const DeltaInfoVector* src, uint ofs, uint size) } } - const DeltaInfo const* vecend = DIV_end(src); + const DeltaInfo* const vecend = DIV_end(src); const uchar* nstream; for( ;cdi < vecend; ++cdi){ nstream = next_delta_info(src->dstream + cdi->dso, &dc); @@ -753,7 +753,7 @@ PyObject* DCL_apply(DeltaChunkList* self, PyObject* args) PyObject* tmpargs = PyTuple_New(1); const uchar* data = TSI_first(&self->istream); - const uchar const* dend = TSI_end(&self->istream); + const uchar* const dend = TSI_end(&self->istream); DeltaChunk dc; DC_init(&dc, 0, 0, 0, NULL); @@ -979,8 +979,8 @@ PyObject* connect_deltas(PyObject *self, PyObject *dstreams) const uchar* data; Py_ssize_t dlen; PyObject_AsReadBuffer(db, (const void**)&data, &dlen); - const uchar const* dstart = data; - const uchar const* dend = data + dlen; + const uchar* const dstart = data; + const uchar* const dend = data + dlen; div.dstream = dstart; if (dlen > pow(2, 32)){ From 17413029b0f780ac94c24ab2b5f527ded6abdd2a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 22 Aug 2015 16:39:34 +0200 Subject: [PATCH 025/158] docs(gitdb): discourage usage of GitDB type --- gitdb/db/git.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gitdb/db/git.py b/gitdb/db/git.py index a4f6f54..7a43d72 100644 --- a/gitdb/db/git.py +++ b/gitdb/db/git.py @@ -22,7 +22,11 @@ class GitDB(FileDBBase, ObjectDBW, CompoundDB): """A git-style object database, which contains all objects in the 'objects' - subdirectory""" + subdirectory + + ``IMPORTANT``: The usage of this implementation is highly discouraged as it fails to release file-handles. + This can be a problem with long-running processes and/or big repositories. + """ # Configuration PackDBCls = PackedDB LooseDBCls = LooseObjectDB From 2389b75280efb1a63e6ea578eae7f897fd4beb1b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 4 Oct 2015 19:17:44 +0200 Subject: [PATCH 026/158] fix(loose): avoid unnecessary file rename on windows This should workaround possible permission issues. Related to https://github.com/gitpython-developers/GitPython/issues/353 --- gitdb/db/loose.py | 13 +++++++++---- gitdb/ext/smmap | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 4732b56..1355e1c 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -226,10 +226,15 @@ def store(self, istream): mkdir(obj_dir) # END handle destination directory # rename onto existing doesn't work on windows - if os.name == 'nt' and isfile(obj_path): - remove(obj_path) - # END handle win322 - rename(tmp_path, obj_path) + if os.name == 'nt': + if isfile(obj_path): + remove(tmp_path) + else: + rename(tmp_path, obj_path) + # end rename only if needed + else: + rename(tmp_path, obj_path) + # END handle win32 # make sure its readable for all ! It started out as rw-- tmp file # but needs to be rwrr diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 84929ed..18e4aea 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 84929ed811142e366d6c5916125302c1419acad6 +Subproject commit 18e4aea23644ea43657cb2e6846b6aaf78720c27 From 490fdc2f27ca91898f09defdf20e1237a5ac12aa Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 30 Nov 2015 08:50:56 +0100 Subject: [PATCH 027/158] fix(distribution): remove redundant self-reference --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8a4cd39..ed4898e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -gitdb -smmap>=0.8.3 \ No newline at end of file +smmap>=0.8.3 From d1996e04dbf4841b853b60c1365f0f5fd28d170c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 28 Mar 2016 09:10:31 +0200 Subject: [PATCH 028/158] Ignore MANIFEST.in Fixes #25 --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 597944f..b939b5d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ include LICENSE include CHANGES include AUTHORS include README +include MANIFEST.in include gitdb/_fun.c include gitdb/_delta_apply.c From 5ff376161a2f2875d3fc0eb1d9f25027e29460ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 27 Jul 2016 08:49:52 +0300 Subject: [PATCH 029/158] travis: Test with Python 3.5 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 8cab822..ba0beaa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" # - "pypy" - won't work as smmap doesn't work (see smmap/.travis.yml for details) git: From cadb3d8a095c3e51cede1c33f7dcbf49c1426418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 27 Jul 2016 08:53:11 +0300 Subject: [PATCH 030/158] setup: Add Python 3.5 classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index e4b1b1d..c2f9f1e 100755 --- a/setup.py +++ b/setup.py @@ -121,4 +121,5 @@ def get_data_files(self): "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", ],) From 2af455266cb3ea454c4f38b182825b9e78b8398d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 27 Jul 2016 09:32:29 +0300 Subject: [PATCH 031/158] Spelling fixes --- doc/source/algorithm.rst | 2 +- doc/source/changes.rst | 2 +- gitdb/db/base.py | 2 +- gitdb/db/loose.py | 2 +- gitdb/db/mem.py | 2 +- gitdb/pack.py | 4 ++-- gitdb/stream.py | 2 +- gitdb/test/db/test_loose.py | 2 +- gitdb/test/lib.py | 2 +- gitdb/test/test_pack.py | 2 +- gitdb/test/test_util.py | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/source/algorithm.rst b/doc/source/algorithm.rst index 4374cb8..2e01b3f 100644 --- a/doc/source/algorithm.rst +++ b/doc/source/algorithm.rst @@ -92,6 +92,6 @@ Future work Another very promising option is that streaming of delta data is indeed possible. Depending on the configuration of the copy-from-base operations, different optimizations could be applied to reduce the amount of memory required for the final processed delta stream. Some configurations may even allow it to stream data from the base buffer, instead of pre-loading it for random access. -The ability to stream files at reduced memory costs would only be feasible for big files, and would have to be payed with extra pre-processing time. +The ability to stream files at reduced memory costs would only be feasible for big files, and would have to be paid with extra pre-processing time. A very first and simple implementation could avoid memory peaks by streaming the TDS in conjunction with a base buffer, instead of writing everything into a fully allocated target buffer. diff --git a/doc/source/changes.rst b/doc/source/changes.rst index a36fd65..22deb6d 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -8,7 +8,7 @@ Changelog * Fixed possibly critical error, see https://github.com/gitpython-developers/GitPython/issues/220 - - However, it only seems to occour on high-entropy data and didn't reoccour after the fix + - However, it only seems to occur on high-entropy data and didn't reoccour after the fix ***** 0.6.0 diff --git a/gitdb/db/base.py b/gitdb/db/base.py index 2615b13..2d7b9fa 100644 --- a/gitdb/db/base.py +++ b/gitdb/db/base.py @@ -177,7 +177,7 @@ def _db_query(self, sha): """:return: database containing the given 20 byte sha :raise BadObject:""" # most databases use binary representations, prevent converting - # it everytime a database is being queried + # it every time a database is being queried try: return self._db_cache[sha] except KeyError: diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 1355e1c..192c524 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -174,7 +174,7 @@ def has_object(self, sha): return True except BadObject: return False - # END check existance + # END check existence def store(self, istream): """note: The sha we produce will be hex by nature""" diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index 595dbf4..8711334 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -98,7 +98,7 @@ def stream_copy(self, sha_iter, odb): for sha in sha_iter: if odb.has_object(sha): continue - # END check object existance + # END check object existence ostream = self.stream(sha) # compressed data including header diff --git a/gitdb/pack.py b/gitdb/pack.py index 511e557..2447455 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -85,7 +85,7 @@ def pack_object_at(cursor, offset, as_stream): an object of the correct type according to the type_id of the object. If as_stream is True, the object will contain a stream, allowing the data to be read decompressed. - :param data: random accessable data containing all required information + :param data: random accessible data containing all required information :parma offset: offset in to the data at which the object information is located :param as_stream: if True, a stream object will be returned that can read the data, otherwise you receive an info object only""" @@ -447,7 +447,7 @@ def partial_sha_to_index(self, partial_bin_sha, canonical_length): :return: index as in `sha_to_index` or None if the sha was not found in this index file :param partial_bin_sha: an at least two bytes of a partial binary sha as bytes - :param canonical_length: lenght of the original hexadecimal representation of the + :param canonical_length: length of the original hexadecimal representation of the given partial binary sha :raise AmbiguousObjectName:""" if len(partial_bin_sha) < 2: diff --git a/gitdb/stream.py b/gitdb/stream.py index 04dd79f..be95c11 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -660,7 +660,7 @@ def __init__(self, fd): def write(self, data): """:raise IOError: If not all bytes could be written - :return: lenght of incoming data""" + :return: length of incoming data""" self.sha1.update(data) cdata = self.zip.compress(data) bytes_written = write(self.fd, cdata) diff --git a/gitdb/test/db/test_loose.py b/gitdb/test/db/test_loose.py index 024c194..9c25a02 100644 --- a/gitdb/test/db/test_loose.py +++ b/gitdb/test/db/test_loose.py @@ -33,4 +33,4 @@ def test_basics(self, path): # END for each sha self.failUnlessRaises(BadObject, ldb.partial_to_complete_sha_hex, '0000') - # raises if no object could be foudn + # raises if no object could be found diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index a089eac..bbdd241 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -76,7 +76,7 @@ def wrapper(self, *args, **kwargs): def with_rw_directory(func): """Create a temporary directory which can be written to, remove it if the - test suceeds, but leave it otherwise to aid additional debugging""" + test succeeds, but leave it otherwise to aid additional debugging""" def wrapper(self): path = tempfile.mktemp(prefix=func.__name__) diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index ff10572..a8b0b60 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -232,7 +232,7 @@ def rewind_streams(): # END verify files exist # END for each packpath, indexpath pair - # verify the packs throughly + # verify the packs thoroughly rewind_streams() entity = PackEntity.create(pack_objs, rw_dir) count = 0 diff --git a/gitdb/test/test_util.py b/gitdb/test/test_util.py index 1dee544..5026f4e 100644 --- a/gitdb/test/test_util.py +++ b/gitdb/test/test_util.py @@ -60,7 +60,7 @@ def test_lockedfd(self): self._cmp_contents(my_file, orig_data) assert not os.path.isfile(lockfilepath) - # additional call doesnt fail + # additional call doesn't fail lfd.commit() lfd.rollback() From f957c812ac3221773058ba2fa8cd38017537da8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 27 Jul 2016 09:34:09 +0300 Subject: [PATCH 032/158] Handle more file open/close with "with" --- gitdb/db/ref.py | 3 ++- gitdb/test/db/test_ref.py | 7 +++---- gitdb/test/test_pack.py | 5 ++--- gitdb/test/test_util.py | 10 +++------- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index 83a9f61..2e3db86 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -41,7 +41,8 @@ def _update_dbs_from_ref_file(self): # try to get as many as possible, don't fail if some are unavailable ref_paths = list() try: - ref_paths = [l.strip() for l in open(self._ref_file, 'r').readlines()] + with open(self._ref_file, 'r') as f: + ref_paths = [l.strip() for l in f] except (OSError, IOError): pass # END handle alternates diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index 6bac245..2049698 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -21,10 +21,9 @@ class TestReferenceDB(TestDBBase): def make_alt_file(self, alt_path, alt_list): """Create an alternates file which contains the given alternates. The list can be empty""" - alt_file = open(alt_path, "wb") - for alt in alt_list: - alt_file.write(alt.encode("utf-8") + "\n".encode("ascii")) - alt_file.close() + with open(alt_path, "wb") as alt_file: + for alt in alt_list: + alt_file.write(alt.encode("utf-8") + "\n".encode("ascii")) @with_rw_directory def test_writing(self, path): diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index ff10572..8ecfcd9 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -197,7 +197,6 @@ def rewind_streams(): obj.stream.seek(0) # END utility for ppath, ipath, num_obj in zip((pack_path, ) * 2, (index_path, None), (len(pack_objs), None)): - pfile = open(ppath, 'wb') iwrite = None if ipath: ifile = open(ipath, 'wb') @@ -210,8 +209,8 @@ def rewind_streams(): # END rewind streams iteration += 1 - pack_sha, index_sha = PackEntity.write_pack(pack_objs, pfile.write, iwrite, object_count=num_obj) - pfile.close() + with open(ppath, 'wb') as pfile: + pack_sha, index_sha = PackEntity.write_pack(pack_objs, pfile.write, iwrite, object_count=num_obj) assert os.path.getsize(ppath) > 100 # verify pack diff --git a/gitdb/test/test_util.py b/gitdb/test/test_util.py index 1dee544..c298920 100644 --- a/gitdb/test/test_util.py +++ b/gitdb/test/test_util.py @@ -25,19 +25,15 @@ def test_basics(self): def _cmp_contents(self, file_path, data): # raise if data from file at file_path # does not match data string - fp = open(file_path, "rb") - try: + with open(file_path, "rb") as fp: assert fp.read() == data.encode("ascii") - finally: - fp.close() def test_lockedfd(self): my_file = tempfile.mktemp() orig_data = "hello" new_data = "world" - my_file_fp = open(my_file, "wb") - my_file_fp.write(orig_data.encode("ascii")) - my_file_fp.close() + with open(my_file, "wb") as my_file_fp: + my_file_fp.write(orig_data.encode("ascii")) try: lfd = LockedFD(my_file) From 36ed59a1bb44ecada33ce83049605fd7d70e7876 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Wed, 7 Sep 2016 14:59:29 +0100 Subject: [PATCH 033/158] Support universal wheels --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3c6e79c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 From fde2dde8e6fe39c8548acb0c919cf18adaf2806a Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 12 Sep 2016 20:16:22 +0100 Subject: [PATCH 034/158] Do not support universal wheels because you need to build manylinux1/mac/windows wheels instead --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79c..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 From 62815b5c5a4c39e9ace3d20ec0c593011201dbcf Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 4 Oct 2016 18:04:24 +0100 Subject: [PATCH 035/158] support optional gitdb_speedups --- README.rst | 7 + gitdb/_delta_apply.c | 1154 ------------------------------------------ gitdb/_delta_apply.h | 6 - gitdb/_fun.c | 107 ---- gitdb/fun.py | 2 +- gitdb/pack.py | 2 +- gitdb/stream.py | 2 +- setup.cfg | 2 + setup.py | 154 ++---- 9 files changed, 49 insertions(+), 1387 deletions(-) delete mode 100644 gitdb/_delta_apply.c delete mode 100644 gitdb/_delta_apply.h delete mode 100644 gitdb/_fun.c create mode 100644 setup.cfg diff --git a/README.rst b/README.rst index 766d9d6..1b754d0 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,13 @@ From `PyPI `_ pip install gitdb +SPEEDUPS +======== + +If you want to go up to 20% faster, you can install gitdb-speedups with: + + pip install gitdb-speedups + REQUIREMENTS ============ diff --git a/gitdb/_delta_apply.c b/gitdb/_delta_apply.c deleted file mode 100644 index f4fffdc..0000000 --- a/gitdb/_delta_apply.c +++ /dev/null @@ -1,1154 +0,0 @@ -#include <_delta_apply.h> -#include -#include -#include -#include -#include - - - -typedef unsigned long long ull; -typedef unsigned int uint; -typedef unsigned char uchar; -typedef unsigned short ushort; -typedef uchar bool; - -// Constants -const ull gDIV_grow_by = 100; - - - -// DELTA STREAM ACCESS -/////////////////////// -inline -ull msb_size(const uchar** datap, const uchar* top) -{ - const uchar *data = *datap; - ull cmd, size = 0; - uint i = 0; - do { - cmd = *data++; - size |= (cmd & 0x7f) << i; - i += 7; - } while (cmd & 0x80 && data < top); - *datap = data; - return size; -} - - -// TOP LEVEL STREAM INFO -///////////////////////////// -typedef struct { - const uchar *tds; // Toplevel delta stream - const uchar *cstart; // start of the chunks - Py_ssize_t tdslen; // size of tds in bytes - Py_ssize_t target_size; // size of the target buffer which can hold all data - uint num_chunks; // amount of chunks in the delta stream - PyObject *parent_object; -} ToplevelStreamInfo; - - -void TSI_init(ToplevelStreamInfo* info) -{ - info->tds = NULL; - info->cstart = NULL; - info->tdslen = 0; - info->num_chunks = 0; - info->target_size = 0; - info->parent_object = 0; -} - -void TSI_destroy(ToplevelStreamInfo* info) -{ -#ifdef DEBUG - fprintf(stderr, "TSI_destroy: %p\n", info); -#endif - - if (info->parent_object){ - Py_DECREF(info->parent_object); - info->parent_object = NULL; - } else if (info->tds){ - PyMem_Free((void*)info->tds); - } - info->tds = NULL; - info->cstart = NULL; - info->tdslen = 0; - info->num_chunks = 0; -} - -inline -const uchar* TSI_end(ToplevelStreamInfo* info) -{ - return info->tds + info->tdslen; -} - -inline -const uchar* TSI_first(ToplevelStreamInfo* info) -{ - return info->cstart; -} - -// set the stream, and initialize it -// initialize our set stream to point to the first chunk -// Fill in the header information, which is the base and target size -inline -void TSI_set_stream(ToplevelStreamInfo* info, const uchar* stream) -{ - info->tds = stream; - info->cstart = stream; - - assert(info->tds && info->tdslen); - - // init stream - const uchar* tdsend = TSI_end(info); - msb_size(&info->cstart, tdsend); // base size - info->target_size = msb_size(&info->cstart, tdsend); -} - - - -// duplicate the data currently owned by the parent object drop its refcount -// return 1 on success -bool TSI_copy_stream_from_object(ToplevelStreamInfo* info) -{ - assert(info->parent_object); - - uchar* ptmp = PyMem_Malloc(info->tdslen); - if (!ptmp){ - return 0; - } - uint ofs = (uint)(info->cstart - info->tds); - memcpy((void*)ptmp, info->tds, info->tdslen); - - info->tds = ptmp; - info->cstart = ptmp + ofs; - - Py_DECREF(info->parent_object); - info->parent_object = 0; - - return 1; -} - -// Transfer ownership of the given stream into our instance. The amount of chunks -// remains the same, and needs to be set by the caller -void TSI_replace_stream(ToplevelStreamInfo* info, const uchar* stream, uint streamlen) -{ - assert(info->parent_object == 0); - - uint ofs = (uint)(info->cstart - info->tds); - if (info->tds){ - PyMem_Free((void*)info->tds); - } - info->tds = stream; - info->cstart = info->tds + ofs; - info->tdslen = streamlen; - -} - -// DELTA CHUNK -//////////////// -// Internal Delta Chunk Objects -// They are just used to keep information parsed from a stream -// The data pointer is always shared -typedef struct { - ull to; - uint ts; - uint so; - const uchar* data; -} DeltaChunk; - -// forward declarations -const uchar* next_delta_info(const uchar*, DeltaChunk*); - -inline -void DC_init(DeltaChunk* dc, ull to, ull ts, ull so, const uchar* data) -{ - dc->to = to; - dc->ts = ts; - dc->so = so; - dc->data = NULL; -} - - -inline -ull DC_rbound(const DeltaChunk* dc) -{ - return dc->to + dc->ts; -} - -inline -void DC_print(const DeltaChunk* dc, const char* prefix) -{ - fprintf(stderr, "%s-dc: to = %i, ts = %i, so = %i, data = %p\n", prefix, (int)dc->to, dc->ts, dc->so, dc->data); -} - -// Apply -inline -void DC_apply(const DeltaChunk* dc, const uchar* base, PyObject* writer, PyObject* tmpargs) -{ - PyObject* buffer = 0; - if (dc->data){ - buffer = PyBuffer_FromMemory((void*)dc->data, dc->ts); - } else { - buffer = PyBuffer_FromMemory((void*)(base + dc->so), dc->ts); - } - - if (PyTuple_SetItem(tmpargs, 0, buffer)){ - assert(0); - } - - - // tuple steals reference, and will take care about the deallocation - PyObject_Call(writer, tmpargs, NULL); - -} - -// Encode the information in the given delta chunk and write the byte-stream -// into the given output stream -// It will be copied into the given bounds, the given size must be the final size -// and work with the given relative offset - hence the bounds are assumed to be -// correct and to fit within the unaltered dc -inline -void DC_encode_to(const DeltaChunk* dc, uchar** pout, uint ofs, uint size) -{ - uchar* out = *pout; - if (dc->data){ - *out++ = (uchar)size; - memcpy(out, dc->data+ofs, size); - out += size; - } else { - uchar i = 0x80; - uchar* op = out++; - uint moff = dc->so+ofs; - - if (moff & 0x000000ff) - *out++ = moff >> 0, i |= 0x01; - if (moff & 0x0000ff00) - *out++ = moff >> 8, i |= 0x02; - if (moff & 0x00ff0000) - *out++ = moff >> 16, i |= 0x04; - if (moff & 0xff000000) - *out++ = moff >> 24, i |= 0x08; - - if (size & 0x00ff) - *out++ = size >> 0, i |= 0x10; - if (size & 0xff00) - *out++ = size >> 8, i |= 0x20; - - *op = i; - } - - *pout = out; -} - -// Return: amount of bytes one would need to encode dc -inline -ushort DC_count_encode_bytes(const DeltaChunk* dc) -{ - if (dc->data){ - return 1 + dc->ts; // cmd byte + actual data bytes - } else { - ushort c = 1; // cmd byte - uint ts = dc->ts; - ull so = dc->so; - - // offset - c += (so & 0x000000FF) > 0; - c += (so & 0x0000FF00) > 0; - c += (so & 0x00FF0000) > 0; - c += (so & 0xFF000000) > 0; - - // size - max size is 0x10000, its encoded with 0 size bits - c += (ts & 0x000000FF) > 0; - c += (ts & 0x0000FF00) > 0; - - return c; - } -} - - - -// DELTA INFO -///////////// -typedef struct { - uint dso; // delta stream offset, relative to the very start of the stream - uint to; // target offset (cache) -} DeltaInfo; - - -// DELTA INFO VECTOR -////////////////////// - -typedef struct { - DeltaInfo *mem; // Memory for delta infos - uint di_last_size; // size of the last element - we can't compute it using the next bound - const uchar *dstream; // borrowed ointer to delta stream we index - Py_ssize_t size; // Amount of DeltaInfos - Py_ssize_t reserved_size; // Reserved amount of DeltaInfos -} DeltaInfoVector; - - - -// Reserve enough memory to hold the given amount of delta chunks -// Return 1 on success -// NOTE: added a minimum allocation to assure reallocation is not done -// just for a single additional entry. DIVs change often, and reallocs are expensive -inline -int DIV_reserve_memory(DeltaInfoVector* vec, uint num_dc) -{ - if (num_dc <= vec->reserved_size){ - return 1; - } - -#ifdef DEBUG - bool was_null = vec->mem == NULL; -#endif - - if (vec->mem == NULL){ - vec->mem = PyMem_Malloc(num_dc * sizeof(DeltaInfo)); - } else { - vec->mem = PyMem_Realloc(vec->mem, num_dc * sizeof(DeltaInfo)); - } - - if (vec->mem == NULL){ - Py_FatalError("Could not allocate memory for append operation"); - } - - vec->reserved_size = num_dc; - -#ifdef DEBUG - const char* format = "Allocated %i bytes at %p, to hold up to %i chunks\n"; - if (!was_null) - format = "Re-allocated %i bytes at %p, to hold up to %i chunks\n"; - fprintf(stderr, format, (int)(vec->reserved_size * sizeof(DeltaInfo)), vec->mem, (int)vec->reserved_size); -#endif - - return vec->mem != NULL; -} - -/* -Grow the delta chunk list by the given amount of bytes. -This may trigger a realloc, but will do nothing if the reserved size is already -large enough. -Return 1 on success, 0 on failure -*/ -inline -int DIV_grow_by(DeltaInfoVector* vec, uint num_dc) -{ - return DIV_reserve_memory(vec, vec->reserved_size + num_dc); -} - -int DIV_init(DeltaInfoVector* vec, ull initial_size) -{ - vec->mem = NULL; - vec->dstream = NULL; - vec->size = 0; - vec->reserved_size = 0; - vec->di_last_size = 0; - - return DIV_grow_by(vec, initial_size); -} - -inline -Py_ssize_t DIV_len(const DeltaInfoVector* vec) -{ - return vec->size; -} - -inline -uint DIV_lbound(const DeltaInfoVector* vec) -{ - assert(vec->size && vec->mem); - return vec->mem->to; -} - -// Return item at index -inline -DeltaInfo* DIV_get(const DeltaInfoVector* vec, Py_ssize_t i) -{ - assert(i < vec->size && vec->mem); - return &vec->mem[i]; -} - -// Return last item -inline -DeltaInfo* DIV_last(const DeltaInfoVector* vec) -{ - return DIV_get(vec, vec->size-1); -} - -inline -int DIV_empty(const DeltaInfoVector* vec) -{ - return vec->size == 0; -} - -// Return end pointer of the vector -inline -const DeltaInfo* DIV_end(const DeltaInfoVector* vec) -{ - assert(!DIV_empty(vec)); - return vec->mem + vec->size; -} - -// return first item in vector -inline -DeltaInfo* DIV_first(const DeltaInfoVector* vec) -{ - assert(!DIV_empty(vec)); - return vec->mem; -} - -// return rbound offset in bytes. We use information contained in the -// vec to do that -inline -uint DIV_info_rbound(const DeltaInfoVector* vec, const DeltaInfo* di) -{ - if (DIV_last(vec) == di){ - return di->to + vec->di_last_size; - } else { - return (di+1)->to; - } -} - -// return size of the given delta info item -inline -uint DIV_info_size2(const DeltaInfoVector* vec, const DeltaInfo* di, const DeltaInfo* const veclast) -{ - if (veclast == di){ - return vec->di_last_size; - } else { - return (di+1)->to - di->to; - } -} - -// return size of the given delta info item -inline -uint DIV_info_size(const DeltaInfoVector* vec, const DeltaInfo* di) -{ - return DIV_info_size2(vec, di, DIV_last(vec)); -} - -void DIV_destroy(DeltaInfoVector* vec) -{ - if (vec->mem){ -#ifdef DEBUG - fprintf(stderr, "DIV_destroy: %p\n", (void*)vec->mem); -#endif - PyMem_Free(vec->mem); - vec->size = 0; - vec->reserved_size = 0; - vec->mem = 0; - } -} - -// Reset this vector so that its existing memory can be filled again. -// Memory will be kept, but not cleaned up -inline -void DIV_forget_members(DeltaInfoVector* vec) -{ - vec->size = 0; -} - -// Reset the vector so that its size will be zero -// It will keep its memory though, and hence can be filled again -inline -void DIV_reset(DeltaInfoVector* vec) -{ - if (vec->size == 0) - return; - vec->size = 0; -} - - -// Append one chunk to the end of the list, and return a pointer to it -// It will not have been initialized ! -inline -DeltaInfo* DIV_append(DeltaInfoVector* vec) -{ - if (vec->size + 1 > vec->reserved_size){ - DIV_grow_by(vec, gDIV_grow_by); - } - - DeltaInfo* next = vec->mem + vec->size; - vec->size += 1; - return next; -} - -// Return delta chunk being closest to the given absolute offset -inline -DeltaInfo* DIV_closest_chunk(const DeltaInfoVector* vec, ull ofs) -{ - assert(vec->mem); - - ull lo = 0; - ull hi = vec->size; - ull mid; - DeltaInfo* di; - - while (lo < hi) - { - mid = (lo + hi) / 2; - di = vec->mem + mid; - if (di->to > ofs){ - hi = mid; - } else if ((DIV_info_rbound(vec, di) > ofs) | (di->to == ofs)) { - return di; - } else { - lo = mid + 1; - } - } - - return DIV_last(vec); -} - - -// Return the amount of chunks a slice at the given spot would have, as well as -// its size in bytes it would have if the possibly partial chunks would be encoded -// and added to the spot marked by sdc -uint DIV_count_slice_bytes(const DeltaInfoVector* src, uint ofs, uint size) -{ - uint num_bytes = 0; - DeltaInfo* cdi = DIV_closest_chunk(src, ofs); - - DeltaChunk dc; - DC_init(&dc, 0, 0, 0, NULL); - - // partial overlap - if (cdi->to != ofs) { - const ull relofs = ofs - cdi->to; - const uint cdisize = DIV_info_size(src, cdi); - const uint max_size = cdisize - relofs < size ? cdisize - relofs : size; - size -= max_size; - - // get the size in bytes the info would have - next_delta_info(src->dstream + cdi->dso, &dc); - dc.so += relofs; - dc.ts = max_size; - num_bytes += DC_count_encode_bytes(&dc); - - cdi += 1; - - if (size == 0){ - return num_bytes; - } - } - - const DeltaInfo* const vecend = DIV_end(src); - const uchar* nstream; - for( ;cdi < vecend; ++cdi){ - nstream = next_delta_info(src->dstream + cdi->dso, &dc); - - if (dc.ts < size) { - num_bytes += nstream - (src->dstream + cdi->dso); - size -= dc.ts; - } else { - dc.ts = size; - num_bytes += DC_count_encode_bytes(&dc); - size = 0; - break; - } - } - - assert(size == 0); - return num_bytes; -} - -// Write a slice as defined by its absolute offset in bytes and its size into the given -// destination memory. The individual chunks written will be a byte copy of the source -// data chunk stream -// Return: number of chunks in the slice -uint DIV_copy_slice_to(const DeltaInfoVector* src, uchar** dest, ull tofs, uint size) -{ - assert(DIV_lbound(src) <= tofs); - assert((tofs + size) <= DIV_info_rbound(src, DIV_last(src))); - - DeltaChunk dc; - DC_init(&dc, 0, 0, 0, NULL); - - DeltaInfo* cdi = DIV_closest_chunk(src, tofs); - uint num_chunks = 0; - - // partial overlap - if (cdi->to != tofs) { - const uint relofs = tofs - cdi->to; - next_delta_info(src->dstream + cdi->dso, &dc); - const uint max_size = dc.ts - relofs < size ? dc.ts - relofs : size; - - size -= max_size; - - // adjust dc proportions - DC_encode_to(&dc, dest, relofs, max_size); - - num_chunks += 1; - cdi += 1; - - if (size == 0){ - return num_chunks; - } - } - - const uchar* dstream = src->dstream + cdi->dso; - const uchar* nstream = dstream; - for( ; nstream; dstream = nstream) - { - num_chunks += 1; - nstream = next_delta_info(dstream, &dc); - if (dc.ts < size) { - memcpy(*dest, dstream, nstream - dstream); - *dest += nstream - dstream; - size -= dc.ts; - } else { - DC_encode_to(&dc, dest, 0, size); - size = 0; - break; - } - } - - assert(size == 0); - return num_chunks; -} - - -// Take slices of div into the corresponding area of the tsi, which is the topmost -// delta to apply. -bool DIV_connect_with_base(ToplevelStreamInfo* tsi, DeltaInfoVector* div) -{ - assert(tsi->num_chunks); - - - uint num_bytes = 0; - const uchar* data = TSI_first(tsi); - const uchar* dend = TSI_end(tsi); - - DeltaChunk dc; - DC_init(&dc, 0, 0, 0, NULL); - - - // COMPUTE SIZE OF TARGET STREAM - ///////////////////////////////// - for (;data < dend;) - { - data = next_delta_info(data, &dc); - - // Data chunks don't need processing - if (dc.data){ - num_bytes += 1 + dc.ts; - continue; - } - - num_bytes += DIV_count_slice_bytes(div, dc.so, dc.ts); - } - assert(DC_rbound(&dc) == tsi->target_size); - - - // GET NEW DELTA BUFFER - //////////////////////// - uchar *const dstream = PyMem_Malloc(num_bytes); - if (!dstream){ - return 0; - } - - - data = TSI_first(tsi); - const uchar *ndata = data; - dend = TSI_end(tsi); - - uint num_chunks = 0; - uchar* ds = dstream; - DC_init(&dc, 0, 0, 0, NULL); - - // pick slices from the delta and put them into the new stream - for (; data < dend; data = ndata) - { - ndata = next_delta_info(data, &dc); - - // Data chunks don't need processing - if (dc.data){ - // just copy it over - memcpy((void*)ds, (void*)data, ndata - data); - ds += ndata - data; - num_chunks += 1; - continue; - } - - // Copy Chunks - num_chunks += DIV_copy_slice_to(div, &ds, dc.so, dc.ts); - } - assert(ds - dstream == num_bytes); - assert(num_chunks >= tsi->num_chunks); - assert(DC_rbound(&dc) == tsi->target_size); - - // finally, replace the streams - TSI_replace_stream(tsi, dstream, num_bytes); - tsi->cstart = dstream; // we have NO header ! - assert(tsi->tds == dstream); - tsi->num_chunks = num_chunks; - - - return 1; - -} - -// DELTA CHUNK LIST (PYTHON) -///////////////////////////// -// Internally, it has nothing to do with a ChunkList anymore though -typedef struct { - PyObject_HEAD - // ----------- - ToplevelStreamInfo istream; - -} DeltaChunkList; - - - -int DCL_init(DeltaChunkList*self, PyObject *args, PyObject *kwds) -{ - if(args && PySequence_Size(args) > 0){ - PyErr_SetString(PyExc_ValueError, "Too many arguments"); - return -1; - } - - TSI_init(&self->istream); - return 0; -} - - -void DCL_dealloc(DeltaChunkList* self) -{ - TSI_destroy(&(self->istream)); -} - - -PyObject* DCL_py_rbound(DeltaChunkList* self) -{ - return PyLong_FromUnsignedLongLong(self->istream.target_size); -} - -// Write using a write function, taking remaining bytes from a base buffer - -PyObject* DCL_apply(DeltaChunkList* self, PyObject* args) -{ - PyObject* pybuf = 0; - PyObject* writeproc = 0; - if (!PyArg_ParseTuple(args, "OO", &pybuf, &writeproc)){ - PyErr_BadArgument(); - return NULL; - } - - if (!PyObject_CheckReadBuffer(pybuf)){ - PyErr_SetString(PyExc_ValueError, "First argument must be a buffer-compatible object, like a string, or a memory map"); - return NULL; - } - - if (!PyCallable_Check(writeproc)){ - PyErr_SetString(PyExc_ValueError, "Second argument must be a writer method with signature write(buf)"); - return NULL; - } - - const uchar* base; - Py_ssize_t baselen; - PyObject_AsReadBuffer(pybuf, (const void**)&base, &baselen); - - PyObject* tmpargs = PyTuple_New(1); - - const uchar* data = TSI_first(&self->istream); - const uchar* const dend = TSI_end(&self->istream); - - DeltaChunk dc; - DC_init(&dc, 0, 0, 0, NULL); - - while (data < dend){ - data = next_delta_info(data, &dc); - DC_apply(&dc, base, writeproc, tmpargs); - } - - Py_DECREF(tmpargs); - Py_RETURN_NONE; -} - -PyMethodDef DCL_methods[] = { - {"apply", (PyCFunction)DCL_apply, METH_VARARGS, "Apply the given iterable of delta streams" }, - {"rbound", (PyCFunction)DCL_py_rbound, METH_NOARGS, NULL}, - {NULL} /* Sentinel */ -}; - -PyTypeObject DeltaChunkListType = { - PyObject_HEAD_INIT(NULL) - 0, /*ob_size*/ - "DeltaChunkList", /*tp_name*/ - sizeof(DeltaChunkList), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor)DCL_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT, /*tp_flags*/ - "Minimal Delta Chunk List",/* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - DCL_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)DCL_init, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ -}; - - -// Makes a new copy of the DeltaChunkList - you have to do everything yourselve -// in C ... want C++ !! -DeltaChunkList* DCL_new_instance(void) -{ - DeltaChunkList* dcl = (DeltaChunkList*) PyType_GenericNew(&DeltaChunkListType, 0, 0); - assert(dcl); - - DCL_init(dcl, 0, 0); - return dcl; -} - -// Read the next delta chunk from the given stream and advance it -// dc will contain the parsed information, its offset must be set by -// the previous call of next_delta_info, which implies it should remain the -// same instance between the calls. -// Return the altered uchar pointer, reassign it to the input data -inline -const uchar* next_delta_info(const uchar* data, DeltaChunk* dc) -{ - const char cmd = *data++; - - if (cmd & 0x80) - { - uint cp_off = 0, cp_size = 0; - if (cmd & 0x01) cp_off = *data++; - if (cmd & 0x02) cp_off |= (*data++ << 8); - if (cmd & 0x04) cp_off |= (*data++ << 16); - if (cmd & 0x08) cp_off |= ((unsigned) *data++ << 24); - if (cmd & 0x10) cp_size = *data++; - if (cmd & 0x20) cp_size |= (*data++ << 8); - if (cmd & 0x40) cp_size |= (*data++ << 16); // this should never get hit with current deltas ... - if (cp_size == 0) cp_size = 0x10000; - - dc->to += dc->ts; - dc->data = NULL; - dc->so = cp_off; - dc->ts = cp_size; - - } else if (cmd) { - // Just share the data - dc->to += dc->ts; - dc->data = data; - dc->ts = cmd; - dc->so = 0; - - data += cmd; - } else { - PyErr_SetString(PyExc_RuntimeError, "Encountered an unsupported delta cmd: 0"); - assert(0); - return NULL; - } - - return data; -} - -// Return amount of chunks encoded in the given delta stream -// If read_header is True, then the header msb chunks will be read first. -// Otherwise, the stream is assumed to be scrubbed one past the header -uint compute_chunk_count(const uchar* data, const uchar* dend, bool read_header) -{ - // read header - if (read_header){ - msb_size(&data, dend); - msb_size(&data, dend); - } - - DeltaChunk dc; - DC_init(&dc, 0, 0, 0, NULL); - uint num_chunks = 0; - - while (data < dend) - { - data = next_delta_info(data, &dc); - num_chunks += 1; - }// END handle command opcodes - - return num_chunks; -} - -PyObject* connect_deltas(PyObject *self, PyObject *dstreams) -{ - // obtain iterator - PyObject* stream_iter = 0; - if (!PyIter_Check(dstreams)){ - stream_iter = PyObject_GetIter(dstreams); - if (!stream_iter){ - PyErr_SetString(PyExc_RuntimeError, "Couldn't obtain iterator for streams"); - return NULL; - } - } else { - stream_iter = dstreams; - } - - DeltaInfoVector div; - ToplevelStreamInfo tdsinfo; - TSI_init(&tdsinfo); - DIV_init(&div, 0); - - - // GET TOPLEVEL DELTA STREAM - int error = 0; - PyObject* ds = 0; - unsigned int dsi = 0; // delta stream index we process - ds = PyIter_Next(stream_iter); - if (!ds){ - error = 1; - goto _error; - } - - dsi += 1; - tdsinfo.parent_object = PyObject_CallMethod(ds, "read", 0); - if (!PyObject_CheckReadBuffer(tdsinfo.parent_object)){ - Py_DECREF(ds); - error = 1; - goto _error; - } - - PyObject_AsReadBuffer(tdsinfo.parent_object, (const void**)&tdsinfo.tds, &tdsinfo.tdslen); - if (tdsinfo.tdslen > pow(2, 32)){ - // parent object is deallocated by info structure - Py_DECREF(ds); - PyErr_SetString(PyExc_RuntimeError, "Cannot handle deltas larger than 4GB"); - tdsinfo.parent_object = 0; - - error = 1; - goto _error; - } - Py_DECREF(ds); - - // let it officially know, and initialize its internal state - TSI_set_stream(&tdsinfo, tdsinfo.tds); - - // INTEGRATE ANCESTOR DELTA STREAMS - for (ds = PyIter_Next(stream_iter); ds != NULL; ds = PyIter_Next(stream_iter), ++dsi) - { - // Its important to initialize this before the next block which can jump - // to code who needs this to exist ! - PyObject* db = 0; - - // When processing the first delta, we know we will have to alter the tds - // Hence we copy it and deallocate the parent object - if (dsi == 1) { - if (!TSI_copy_stream_from_object(&tdsinfo)){ - PyErr_SetString(PyExc_RuntimeError, "Could not allocate memory to copy toplevel buffer"); - // info structure takes care of the parent_object - error = 1; - goto loop_end; - } - - tdsinfo.num_chunks = compute_chunk_count(tdsinfo.cstart, TSI_end(&tdsinfo), 0); - } - - db = PyObject_CallMethod(ds, "read", 0); - if (!PyObject_CheckReadBuffer(db)){ - error = 1; - PyErr_SetString(PyExc_RuntimeError, "Returned buffer didn't support the buffer protocol"); - goto loop_end; - } - - // Fill the stream info structure - const uchar* data; - Py_ssize_t dlen; - PyObject_AsReadBuffer(db, (const void**)&data, &dlen); - const uchar* const dstart = data; - const uchar* const dend = data + dlen; - div.dstream = dstart; - - if (dlen > pow(2, 32)){ - error = 1; - PyErr_SetString(PyExc_RuntimeError, "Cannot currently handle deltas larger than 4GB"); - goto loop_end; - } - - // READ HEADER - msb_size(&data, dend); - const ull target_size = msb_size(&data, dend); - - DIV_reserve_memory(&div, compute_chunk_count(data, dend, 0)); - - // parse command stream - DeltaInfo* di = 0; // temporary pointer - DeltaChunk dc; - DC_init(&dc, 0, 0, 0, NULL); - - assert(data < dend); - while (data < dend) - { - di = DIV_append(&div); - di->dso = data - dstart; - if ((data = next_delta_info(data, &dc))){ - di->to = dc.to; - } else { - error = 1; - goto loop_end; - } - }// END handle command opcodes - - // finalize information - div.di_last_size = dc.ts; - - if (DC_rbound(&dc) != target_size){ - PyErr_SetString(PyExc_RuntimeError, "Failed to parse delta stream"); - error = 1; - } - - #ifdef DEBUG - fprintf(stderr, "------------ Stream %i --------\n ", (int)dsi); - fprintf(stderr, "Before Connect: tdsinfo: num_chunks = %i, bytelen = %i KiB, target_size = %i KiB\n", (int)tdsinfo.num_chunks, (int)tdsinfo.tdslen/1000, (int)tdsinfo.target_size/1000); - fprintf(stderr, "div->num_chunks = %i, div->reserved_size = %i, div->bytelen=%i KiB\n", (int)div.size, (int)div.reserved_size, (int)dlen/1000); - #endif - - if (!DIV_connect_with_base(&tdsinfo, &div)){ - error = 1; - } - - #ifdef DEBUG - fprintf(stderr, "after connect: tdsinfo->num_chunks = %i, tdsinfo->bytelen = %i KiB\n", (int)tdsinfo.num_chunks, (int)tdsinfo.tdslen/1000); - #endif - - // destroy members, but keep memory - DIV_reset(&div); - -loop_end: - // perform cleanup - Py_DECREF(ds); - Py_DECREF(db); - - if (error){ - break; - } - }// END for each stream object - - if (dsi == 0){ - PyErr_SetString(PyExc_ValueError, "No streams provided"); - } - - -_error: - - if (stream_iter != dstreams){ - Py_DECREF(stream_iter); - } - - - DIV_destroy(&div); - - // Return the actual python object - its just a container - DeltaChunkList* dcl = DCL_new_instance(); - if (!dcl){ - PyErr_SetString(PyExc_RuntimeError, "Couldn't allocate list"); - // Otherwise tdsinfo would be deallocated by the chunk list - TSI_destroy(&tdsinfo); - error = 1; - } else { - // Plain copy, transfer ownership to dcl - dcl->istream = tdsinfo; - } - - if (error){ - // Will dealloc tdcv - Py_XDECREF(dcl); - return NULL; - } - - return (PyObject*)dcl; -} - - -// Write using a write function, taking remaining bytes from a base buffer -// replaces the corresponding method in python -PyObject* apply_delta(PyObject* self, PyObject* args) -{ - PyObject* pybbuf = 0; - PyObject* pydbuf = 0; - PyObject* pytbuf = 0; - if (!PyArg_ParseTuple(args, "OOO", &pybbuf, &pydbuf, &pytbuf)){ - PyErr_BadArgument(); - return NULL; - } - - PyObject* objects[] = { pybbuf, pydbuf, pytbuf }; - assert(sizeof(objects) / sizeof(PyObject*) == 3); - - uint i; - for(i = 0; i < 3; i++){ - if (!PyObject_CheckReadBuffer(objects[i])){ - PyErr_SetString(PyExc_ValueError, "Argument must be a buffer-compatible object, like a string, or a memory map"); - return NULL; - } - } - - Py_ssize_t lbbuf; Py_ssize_t ldbuf; Py_ssize_t ltbuf; - const uchar* bbuf; const uchar* dbuf; - uchar* tbuf; - PyObject_AsReadBuffer(pybbuf, (const void**)(&bbuf), &lbbuf); - PyObject_AsReadBuffer(pydbuf, (const void**)(&dbuf), &ldbuf); - - if (PyObject_AsWriteBuffer(pytbuf, (void**)(&tbuf), <buf)){ - PyErr_SetString(PyExc_ValueError, "Argument 3 must be a writable buffer"); - return NULL; - } - - const uchar* data = dbuf; - const uchar* dend = dbuf + ldbuf; - - while (data < dend) - { - const char cmd = *data++; - - if (cmd & 0x80) - { - unsigned long cp_off = 0, cp_size = 0; - if (cmd & 0x01) cp_off = *data++; - if (cmd & 0x02) cp_off |= (*data++ << 8); - if (cmd & 0x04) cp_off |= (*data++ << 16); - if (cmd & 0x08) cp_off |= ((unsigned) *data++ << 24); - if (cmd & 0x10) cp_size = *data++; - if (cmd & 0x20) cp_size |= (*data++ << 8); - if (cmd & 0x40) cp_size |= (*data++ << 16); - if (cp_size == 0) cp_size = 0x10000; - - memcpy(tbuf, bbuf + cp_off, cp_size); - tbuf += cp_size; - - } else if (cmd) { - memcpy(tbuf, data, cmd); - tbuf += cmd; - data += cmd; - } else { - PyErr_SetString(PyExc_RuntimeError, "Encountered an unsupported delta cmd: 0"); - return NULL; - } - }// END handle command opcodes - - Py_RETURN_NONE; -} diff --git a/gitdb/_delta_apply.h b/gitdb/_delta_apply.h deleted file mode 100644 index 1fcd538..0000000 --- a/gitdb/_delta_apply.h +++ /dev/null @@ -1,6 +0,0 @@ -#include - -extern PyObject* connect_deltas(PyObject *self, PyObject *dstreams); -extern PyObject* apply_delta(PyObject* self, PyObject* args); - -extern PyTypeObject DeltaChunkListType; diff --git a/gitdb/_fun.c b/gitdb/_fun.c deleted file mode 100644 index 4997038..0000000 --- a/gitdb/_fun.c +++ /dev/null @@ -1,107 +0,0 @@ -#include -#include "_delta_apply.h" - -static PyObject *PackIndexFile_sha_to_index(PyObject *self, PyObject *args) -{ - const unsigned char *sha; - const unsigned int sha_len; - - // Note: self is only set if we are a c type. We emulate an instance method, - // hence we have to get the instance as 'first' argument - - // get instance and sha - PyObject* inst = 0; - if (!PyArg_ParseTuple(args, "Os#", &inst, &sha, &sha_len)) - return NULL; - - if (sha_len != 20) { - PyErr_SetString(PyExc_ValueError, "Sha is not 20 bytes long"); - return NULL; - } - - if( !inst){ - PyErr_SetString(PyExc_ValueError, "Cannot be called without self"); - return NULL; - } - - // read lo and hi bounds - PyObject* fanout_table = PyObject_GetAttrString(inst, "_fanout_table"); - if (!fanout_table){ - PyErr_SetString(PyExc_ValueError, "Couldn't obtain fanout table"); - return NULL; - } - - unsigned int lo = 0, hi = 0; - if (sha[0]){ - PyObject* item = PySequence_GetItem(fanout_table, (const Py_ssize_t)(sha[0]-1)); - lo = PyInt_AS_LONG(item); - Py_DECREF(item); - } - PyObject* item = PySequence_GetItem(fanout_table, (const Py_ssize_t)sha[0]); - hi = PyInt_AS_LONG(item); - Py_DECREF(item); - item = 0; - - Py_DECREF(fanout_table); - - // get sha query function - PyObject* get_sha = PyObject_GetAttrString(inst, "sha"); - if (!get_sha){ - PyErr_SetString(PyExc_ValueError, "Couldn't obtain sha method"); - return NULL; - } - - PyObject *sha_str = 0; - while (lo < hi) { - const int mid = (lo + hi)/2; - sha_str = PyObject_CallFunction(get_sha, "i", mid); - if (!sha_str) { - return NULL; - } - - // we really trust that string ... for speed - const int cmp = memcmp(PyString_AS_STRING(sha_str), sha, 20); - Py_DECREF(sha_str); - sha_str = 0; - - if (cmp < 0){ - lo = mid + 1; - } - else if (cmp > 0) { - hi = mid; - } - else { - Py_DECREF(get_sha); - return PyInt_FromLong(mid); - }// END handle comparison - }// END while lo < hi - - // nothing found, cleanup - Py_DECREF(get_sha); - Py_RETURN_NONE; -} - -static PyMethodDef py_fun[] = { - { "PackIndexFile_sha_to_index", (PyCFunction)PackIndexFile_sha_to_index, METH_VARARGS, "TODO" }, - { "connect_deltas", (PyCFunction)connect_deltas, METH_O, "See python implementation" }, - { "apply_delta", (PyCFunction)apply_delta, METH_VARARGS, "See python implementation" }, - { NULL, NULL, 0, NULL } -}; - -#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ -#define PyMODINIT_FUNC void -#endif -PyMODINIT_FUNC init_perf(void) -{ - PyObject *m; - - if (PyType_Ready(&DeltaChunkListType) < 0) - return; - - m = Py_InitModule3("_perf", py_fun, NULL); - if (m == NULL) - return; - - Py_INCREF(&DeltaChunkListType); - PyModule_AddObject(m, "DeltaChunkList", (PyObject *)&DeltaChunkListType); -} diff --git a/gitdb/fun.py b/gitdb/fun.py index ac9d993..8ca38c8 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -776,6 +776,6 @@ def is_equal_canonical_sha(canonical_length, match, sha1): try: - from _perf import connect_deltas + from gitdb_speedups._perf import connect_deltas except ImportError: pass diff --git a/gitdb/pack.py b/gitdb/pack.py index 2447455..20a4515 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -35,7 +35,7 @@ ) try: - from _perf import PackIndexFile_sha_to_index + from gitdb_speedups._perf import PackIndexFile_sha_to_index except ImportError: pass # END try c module diff --git a/gitdb/stream.py b/gitdb/stream.py index be95c11..2f4c12d 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -33,7 +33,7 @@ has_perf_mod = False PY26 = sys.version_info[:2] < (2, 7) try: - from _perf import apply_delta as c_apply_delta + from gitdb_speedups._perf import apply_delta as c_apply_delta has_perf_mod = True except ImportError: pass diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3c6e79c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py index c2f9f1e..eccb952 100755 --- a/setup.py +++ b/setup.py @@ -1,125 +1,45 @@ -#!/usr/bin/env python -from distutils.core import setup, Extension -from distutils.command.build_py import build_py -from distutils.command.build_ext import build_ext +from setuptools import setup -import os -import sys +# NOTE: This is currently duplicated from the gitdb.__init__ module, because +# that's just how you write a setup.py (nobody reads this stuff out of the +# module) -# wow, this is a mixed bag ... I am pretty upset about all of this ... -setuptools_build_py_module = None -try: - # don't pull it in if we don't have to - if 'setuptools' in sys.modules: - import setuptools.command.build_py as setuptools_build_py_module - from setuptools.command.build_ext import build_ext -except ImportError: - pass - - -class build_ext_nofail(build_ext): - - """Doesn't fail when build our optional extensions""" - - def run(self): - try: - build_ext.run(self) - except Exception: - print("Ignored failure when building extensions, pure python modules will be used instead") - # END ignore errors - - -def get_data_files(self): - """Can you feel the pain ? So, in python2.5 and python2.4 coming with maya, - the line dealing with the ``plen`` has a bug which causes it to truncate too much. - It is fixed in the system interpreters as they receive patches, and shows how - bad it is if something doesn't have proper unittests. - The code here is a plain copy of the python2.6 version which works for all. - - Generate list of '(package,src_dir,build_dir,filenames)' tuples""" - data = [] - if not self.packages: - return data - - # this one is just for the setup tools ! They don't iniitlialize this variable - # when they should, but do it on demand using this method.Its crazy - if hasattr(self, 'analyze_manifest'): - self.analyze_manifest() - # END handle setuptools ... - - for package in self.packages: - # Locate package source directory - src_dir = self.get_package_dir(package) - - # Compute package build directory - build_dir = os.path.join(*([self.build_lib] + package.split('.'))) - - # Length of path to strip from found files - plen = 0 - if src_dir: - plen = len(src_dir) + 1 - - # Strip directory from globbed filenames - filenames = [ - file[plen:] for file in self.find_data_files(package, src_dir) - ] - data.append((package, src_dir, build_dir, filenames)) - return data - -build_py.get_data_files = get_data_files -if setuptools_build_py_module: - setuptools_build_py_module.build_py._get_data_files = get_data_files -# END apply setuptools patch too - -# NOTE: This is currently duplicated from the gitdb.__init__ module, as we cannot -# satisfy the dependencies at installation time, unfortunately, due to inherent limitations -# of distutils, which cannot install the prerequesites of a package before the acutal package. __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" version_info = (0, 6, 4) __version__ = '.'.join(str(i) for i in version_info) -setup(cmdclass={'build_ext': build_ext_nofail}, - name="gitdb", - version=__version__, - description="Git Object Database", - author=__author__, - author_email=__contact__, - url=__homepage__, - packages=('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), - package_dir = {'gitdb': 'gitdb'}, - ext_modules=[Extension('gitdb._perf', ['gitdb/_fun.c', 'gitdb/_delta_apply.c'], include_dirs=['gitdb'])], - license = "BSD License", - zip_safe=False, - requires=('smmap (>=0.8.5)', ), - install_requires=('smmap >= 0.8.5'), - long_description = """GitDB is a pure-Python git object database""", - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - # Picked from - # http://pypi.python.org/pypi?:action=list_classifiers - #"Development Status :: 1 - Planning", - #"Development Status :: 2 - Pre-Alpha", - #"Development Status :: 3 - Alpha", - # "Development Status :: 4 - Beta", - "Development Status :: 5 - Production/Stable", - #"Development Status :: 6 - Mature", - #"Development Status :: 7 - Inactive", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Operating System :: MacOS :: MacOS X", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", -],) +setup( + name="gitdb", + version=__version__, + description="Git Object Database", + author=__author__, + author_email=__contact__, + url=__homepage__, + packages=('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), + license="BSD License", + zip_safe=False, + install_requires=['smmap >= 0.8.5'], + long_description="""GitDB is a pure-Python git object database""", + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + ] +) From 38866bc7c4956170c681a62c4508f934ac826469 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 16 Oct 2016 12:23:27 +0200 Subject: [PATCH 036/158] chore(rename): gitdb2 v2.0 v2 is chosen to better match the name. --- gitdb/__init__.py | 2 +- gitdb/ext/smmap | 2 +- setup.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 6554cf9..bfa083b 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 4) +version_info = (2, 0, 0) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 18e4aea..ac5df70 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 18e4aea23644ea43657cb2e6846b6aaf78720c27 +Subproject commit ac5df7061ee11232346b3d0eb3aa5b43eebc847d diff --git a/setup.py b/setup.py index eccb952..f7e3761 100755 --- a/setup.py +++ b/setup.py @@ -7,11 +7,11 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (0, 6, 4) +version_info = (2, 0, 0) __version__ = '.'.join(str(i) for i in version_info) setup( - name="gitdb", + name="gitdb2", version=__version__, description="Git Object Database", author=__author__, @@ -20,7 +20,7 @@ packages=('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), license="BSD License", zip_safe=False, - install_requires=['smmap >= 0.8.5'], + install_requires=['smmap2 >= 2.0.0'], long_description="""GitDB is a pure-Python git object database""", # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ From 6a217abbbf1673ab2e5794a3cc0bc151e16b9bc0 Mon Sep 17 00:00:00 2001 From: Kostis Anagnostopoulos Date: Sat, 1 Oct 2016 22:28:40 +0200 Subject: [PATCH 037/158] ci: Test on Appveyor for Windows. --- .appveyor.yml | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..2daadaa --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,49 @@ +# CI on Windows via appveyor +environment: + + matrix: + ## MINGW + # + - PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7" + - PYTHON: "C:\\Python34-x64" + PYTHON_VERSION: "3.4" + - PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5" + - PYTHON: "C:\\Miniconda35-x64" + PYTHON_VERSION: "3.5" + IS_CONDA: "yes" + +install: + - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% + + ## Print configuration for debugging. + # + - | + echo %PATH% + uname -a + where python pip pip2 pip3 pip34 + python --version + python -c "import struct; print(struct.calcsize('P') * 8)" + + - IF "%IS_CONDA%"=="yes" ( + conda info -a & + conda install --yes --quiet pip + ) + - pip install nose wheel coveralls + + ## For commits performed with the default user. + - | + git config --global user.email "travis@ci.com" + git config --global user.name "Travis Runner" + + - pip install -e . + +build: false + +test_script: + - IF "%PYTHON_VERSION%"=="3.5" ( + nosetests -v --with-coverage + ) ELSE ( + nosetests -v + ) From 62202bbbb58379814c44bd26cf662e68d3fa6dbb Mon Sep 17 00:00:00 2001 From: Kostis Anagnostopoulos Date: Sat, 1 Oct 2016 22:34:51 +0200 Subject: [PATCH 038/158] appveyor: Add badge on ankostis repo for testing. --- README.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 1b754d0..ca51dfa 100644 --- a/README.rst +++ b/README.rst @@ -43,8 +43,8 @@ Once the clone is complete, please be sure to initialize the submodules using cd gitdb git submodule update --init -Run the tests with - +Run the tests with + nosetests DEVELOPMENT @@ -52,13 +52,12 @@ DEVELOPMENT .. image:: https://travis-ci.org/gitpython-developers/gitdb.svg?branch=master :target: https://travis-ci.org/gitpython-developers/gitdb - +.. image:: https://ci.appveyor.com/api/projects/status/2qa4km4ln7bfv76r/branch/master?svg=true&passingText=windows%20OK&failingText=windows%20failed + :target: https://ci.appveyor.com/project/ankostis/gitpython/branch/master) .. image:: https://coveralls.io/repos/gitpython-developers/gitdb/badge.png :target: https://coveralls.io/r/gitpython-developers/gitdb - .. image:: http://www.issuestats.com/github/gitpython-developers/gitdb/badge/pr :target: http://www.issuestats.com/github/gitpython-developers/gitdb - .. image:: http://www.issuestats.com/github/gitpython-developers/gitdb/badge/issue :target: http://www.issuestats.com/github/gitpython-developers/gitdb From 587dc4ec1311e135d70996a077a2f978e303d3fc Mon Sep 17 00:00:00 2001 From: Kostis Anagnostopoulos Date: Sat, 1 Oct 2016 23:08:31 +0200 Subject: [PATCH 039/158] TCs: fix div-by-zero on elapsed times (appveyor CPU is fast!) --- gitdb/test/performance/test_pack.py | 10 +++++----- gitdb/test/performance/test_pack_streaming.py | 6 +++--- gitdb/test/performance/test_stream.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index bdd2b0a..fc8d9d5 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -36,7 +36,7 @@ def test_pack_random_access(self): sha_list = list(pdb.sha_iter()) elapsed = time() - st ns = len(sha_list) - print("PDB: looked up %i shas by index in %f s ( %f shas/s )" % (ns, elapsed, ns / elapsed), file=sys.stderr) + print("PDB: looked up %i shas by index in %f s ( %f shas/s )" % (ns, elapsed, ns / (elapsed or 1)), file=sys.stderr) # sha lookup: best-case and worst case access pdb_pack_info = pdb._pack_info @@ -51,7 +51,7 @@ def test_pack_random_access(self): del(pdb._entities) pdb.entities() print("PDB: looked up %i sha in %i packs in %f s ( %f shas/s )" % - (ns, len(pdb.entities()), elapsed, ns / elapsed), file=sys.stderr) + (ns, len(pdb.entities()), elapsed, ns / (elapsed or 1)), file=sys.stderr) # END for each random mode # query info and streams only @@ -62,7 +62,7 @@ def test_pack_random_access(self): pdb_fun(sha) elapsed = time() - st print("PDB: Obtained %i object %s by sha in %f s ( %f items/s )" % - (max_items, pdb_fun.__name__.upper(), elapsed, max_items / elapsed), file=sys.stderr) + (max_items, pdb_fun.__name__.upper(), elapsed, max_items / (elapsed or 1)), file=sys.stderr) # END for each function # retrieve stream and read all @@ -78,7 +78,7 @@ def test_pack_random_access(self): elapsed = time() - st total_kib = total_size / 1000 print("PDB: Obtained %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % - (max_items, total_kib, total_kib / elapsed, elapsed, max_items / elapsed), file=sys.stderr) + (max_items, total_kib, total_kib / (elapsed or 1), elapsed, max_items / (elapsed or 1)), file=sys.stderr) @skip_on_travis_ci def test_loose_correctness(self): @@ -129,5 +129,5 @@ def test_correctness(self): # END for each entity elapsed = time() - st print("PDB: verified %i objects (crc=%i) in %f s ( %f objects/s )" % - (count, crc, elapsed, count / elapsed), file=sys.stderr) + (count, crc, elapsed, count / (elapsed or 1)), file=sys.stderr) # END for each verify mode diff --git a/gitdb/test/performance/test_pack_streaming.py b/gitdb/test/performance/test_pack_streaming.py index f805e59..76f0f4a 100644 --- a/gitdb/test/performance/test_pack_streaming.py +++ b/gitdb/test/performance/test_pack_streaming.py @@ -52,14 +52,14 @@ def test_pack_writing(self): # END gather objects for pack-writing elapsed = time() - st print("PDB Streaming: Got %i streams by sha in in %f s ( %f streams/s )" % - (ni, elapsed, ni / elapsed), file=sys.stderr) + (ni, elapsed, ni / (elapsed or 1)), file=sys.stderr) st = time() PackEntity.write_pack((pdb.stream(sha) for sha in pdb.sha_iter()), ostream.write, object_count=ni) elapsed = time() - st total_kb = ostream.bytes_written() / 1000 print(sys.stderr, "PDB Streaming: Wrote pack of size %i kb in %f s (%f kb/s)" % - (total_kb, elapsed, total_kb / elapsed), sys.stderr) + (total_kb, elapsed, total_kb / (elapsed or 1)), sys.stderr) @skip_on_travis_ci def test_stream_reading(self): @@ -82,4 +82,4 @@ def test_stream_reading(self): elapsed = time() - st total_kib = total_size / 1000 print(sys.stderr, "PDB Streaming: Got %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % - (ni, total_kib, total_kib / elapsed, elapsed, ni / elapsed), sys.stderr) + (ni, total_kib, total_kib / (elapsed or 1), elapsed, ni / (elapsed or 1)), sys.stderr) diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index bd66b26..704f4d0 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -70,7 +70,7 @@ def test_large_data_streaming(self, path): size_kib = size / 1000 print("Added %i KiB (filesize = %i KiB) of %s data to loose odb in %f s ( %f Write KiB / s)" % - (size_kib, fsize_kib, desc, elapsed_add, size_kib / elapsed_add), file=sys.stderr) + (size_kib, fsize_kib, desc, elapsed_add, size_kib / (elapsed_add or 1)), file=sys.stderr) # reading all at once st = time() @@ -81,7 +81,7 @@ def test_large_data_streaming(self, path): stream.seek(0) assert shadata == stream.getvalue() print("Read %i KiB of %s data at once from loose odb in %f s ( %f Read KiB / s)" % - (size_kib, desc, elapsed_readall, size_kib / elapsed_readall), file=sys.stderr) + (size_kib, desc, elapsed_readall, size_kib / (elapsed_readall or 1)), file=sys.stderr) # reading in chunks of 1 MiB cs = 512 * 1000 @@ -101,7 +101,7 @@ def test_large_data_streaming(self, path): cs_kib = cs / 1000 print("Read %i KiB of %s data in %i KiB chunks from loose odb in %f s ( %f Read KiB / s)" % - (size_kib, desc, cs_kib, elapsed_readchunks, size_kib / elapsed_readchunks), file=sys.stderr) + (size_kib, desc, cs_kib, elapsed_readchunks, size_kib / (elapsed_readchunks or 1)), file=sys.stderr) # del db file so we keep something to do os.remove(db_file) From d48679a0b15feae754ebe9ef4c9d5809db0c0d08 Mon Sep 17 00:00:00 2001 From: Kostis Anagnostopoulos Date: Sun, 2 Oct 2016 01:10:47 +0200 Subject: [PATCH 040/158] tc: HALF FIX of `test_pack_entity ()` + On Windows, you cannot write onto a file held by another live file-pointer (test_pack.py:#L204). + The TC fails later, on clean up (the usual). --- gitdb/test/test_pack.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 601c0ea..6e31363 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -188,7 +188,8 @@ def test_pack_entity(self, rw_dir): # pack writing - write all packs into one # index path can be None - pack_path = tempfile.mktemp('', "pack", rw_dir) + pack_path1 = tempfile.mktemp('', "pack1", rw_dir) + pack_path2 = tempfile.mktemp('', "pack2", rw_dir) index_path = tempfile.mktemp('', 'index', rw_dir) iteration = 0 @@ -196,7 +197,9 @@ def rewind_streams(): for obj in pack_objs: obj.stream.seek(0) # END utility - for ppath, ipath, num_obj in zip((pack_path, ) * 2, (index_path, None), (len(pack_objs), None)): + for ppath, ipath, num_obj in zip((pack_path1, pack_path2), + (index_path, None), + (len(pack_objs), None)): iwrite = None if ipath: ifile = open(ipath, 'wb') @@ -214,7 +217,7 @@ def rewind_streams(): assert os.path.getsize(ppath) > 100 # verify pack - pf = PackFile(ppath) + pf = PackFile(ppath) # FIXME: Leaks file-pointer(s)! assert pf.size() == len(pack_objs) assert pf.version() == PackFile.pack_version_default assert pf.checksum() == pack_sha From 30329354b370ed6bfac74290ce0c5d2ab17307d1 Mon Sep 17 00:00:00 2001 From: stuertz Date: Sun, 26 Mar 2017 15:45:10 +0200 Subject: [PATCH 041/158] Skip Test on Windows Currently renaming files is not supported while the the OS doesn't support renaming open files. When closing the file, as done in the code by using http://smmap.readthedocs.io/en/latest/api.html#smmap.mman.StaticWindowMapManager.force_map_handle_removal_win force_map_handle_removal_win, we can rename, but the cache does still have a handle to this file and crashes. --- gitdb/test/db/test_pack.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index a901581..e6c2032 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -10,13 +10,17 @@ from gitdb.db import PackedDB from gitdb.exc import BadObject, AmbiguousObjectName +from gitdb.util import mman import os import random +import sys +from unittest import skipIf class TestPackDB(TestDBBase): + @skipIf(sys.platform == "win32", "not supported on windows currently") @with_rw_directory @with_packs_rw def test_writing(self, path): @@ -30,6 +34,11 @@ def test_writing(self, path): # packs removed - rename a file, should affect the glob pack_path = pdb.entities()[0].pack().path() new_pack_path = pack_path + "renamed" + if sys.platform == "win32": + # This is just the beginning: While using thsi function, we are not + # allowed to have any handle to thsi path, which is currently not + # the case. The pack caching does have a handle :-( + mman.force_map_handle_removal_win(pack_path) os.rename(pack_path, new_pack_path) pdb.update_cache(force=True) From 57c3a4fc59b6babe71859b2bf92b2b2fc909ce2a Mon Sep 17 00:00:00 2001 From: stuertz Date: Sun, 26 Mar 2017 15:48:07 +0200 Subject: [PATCH 042/158] Fixed Tests / Code for Windows. Sometimes the OS or some other process has the handle to file a bit longer, and the file could not be deleted immediatly. Retry 10 Times with 100ms distance. --- gitdb/test/performance/test_stream.py | 4 ++-- gitdb/util.py | 27 +++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index 704f4d0..bd8953e 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -9,7 +9,7 @@ from gitdb.db import LooseObjectDB from gitdb import IStream -from gitdb.util import bin_to_hex +from gitdb.util import bin_to_hex, remove from gitdb.fun import chunk_size from time import time @@ -104,5 +104,5 @@ def test_large_data_streaming(self, path): (size_kib, desc, cs_kib, elapsed_readchunks, size_kib / (elapsed_readchunks or 1)), file=sys.stderr) # del db file so we keep something to do - os.remove(db_file) + remove(db_file) # END for each randomization factor diff --git a/gitdb/util.py b/gitdb/util.py index 242be44..95ab9b2 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -6,6 +6,7 @@ import os import mmap import sys +import time import errno from io import BytesIO @@ -58,7 +59,6 @@ def unpack_from(fmt, data, offset=0): isdir = os.path.isdir isfile = os.path.isfile rename = os.rename -remove = os.remove dirname = os.path.dirname basename = os.path.basename join = os.path.join @@ -67,6 +67,25 @@ def unpack_from(fmt, data, offset=0): close = os.close fsync = os.fsync + +def _retry(func, *args, **kwargs): + # Wrapper around functions, that are problematic on "Windows". Sometimes + # the OS or someone else has still a handle to the file + if sys.platform == "win32": + for _ in xrange(10): + try: + return func(*args, **kwargs) + except Exception: + time.sleep(0.1) + return func(*args, **kwargs) + else: + return func(*args, **kwargs) + + +def remove(*args, **kwargs): + return _retry(os.remove, *args, **kwargs) + + # Backwards compatibility imports from gitdb.const import ( NULL_BIN_SHA, @@ -321,7 +340,7 @@ def open(self, write=False, stream=False): self._fd = os.open(self._filepath, os.O_RDONLY | binary) except: # assure we release our lockfile - os.remove(self._lockfilepath()) + remove(self._lockfilepath()) raise # END handle lockfile # END open descriptor for reading @@ -365,7 +384,7 @@ def _end_writing(self, successful=True): # on windows, rename does not silently overwrite the existing one if sys.platform == "win32": if isfile(self._filepath): - os.remove(self._filepath) + remove(self._filepath) # END remove if exists # END win32 special handling os.rename(lockfile, self._filepath) @@ -376,7 +395,7 @@ def _end_writing(self, successful=True): chmod(self._filepath, int("644", 8)) else: # just delete the file so far, we failed - os.remove(lockfile) + remove(lockfile) # END successful handling #} END utilities From 060e5ea18b010e4d8441a5cfad699aba69593c72 Mon Sep 17 00:00:00 2001 From: stuertz Date: Sun, 26 Mar 2017 22:50:56 +0200 Subject: [PATCH 043/158] Release the file handle, before deleting, otherwise win fails. --- gitdb/test/performance/test_stream.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index bd8953e..92d28e4 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -104,5 +104,6 @@ def test_large_data_streaming(self, path): (size_kib, desc, cs_kib, elapsed_readchunks, size_kib / (elapsed_readchunks or 1)), file=sys.stderr) # del db file so we keep something to do + ostream = None # To release the file handle (win) remove(db_file) # END for each randomization factor From d6c097eaf759dabfd3c42b55958962b6e9a05063 Mon Sep 17 00:00:00 2001 From: stuertz Date: Mon, 27 Mar 2017 11:20:48 +0200 Subject: [PATCH 044/158] close smmap handles, to be able to delete files / trees in process (req. for windows) --- gitdb/pack.py | 12 ++++++++++++ gitdb/test/test_pack.py | 7 +++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/gitdb/pack.py b/gitdb/pack.py index 20a4515..115d943 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -266,6 +266,10 @@ def __init__(self, indexpath): super(PackIndexFile, self).__init__() self._indexpath = indexpath + def close(self): + mman.force_map_handle_removal_win(self._indexpath) + self._cursor = None + def _set_cache_(self, attr): if attr == "_packfile_checksum": self._packfile_checksum = self._cursor.map()[-40:-20] @@ -527,6 +531,10 @@ class PackFile(LazyMixin): def __init__(self, packpath): self._packpath = packpath + def close(self): + mman.force_map_handle_removal_win(self._packpath) + self._cursor = None + def _set_cache_(self, attr): # we fill the whole cache, whichever attribute gets queried first self._cursor = mman.make_cursor(self._packpath).use_region() @@ -668,6 +676,10 @@ def __init__(self, pack_or_index_path): self._index = self.IndexFileCls("%s.idx" % basename) # PackIndexFile instance self._pack = self.PackFileCls("%s.pack" % basename) # corresponding PackFile instance + def close(self): + self._index.close() + self._pack.close() + def _set_cache_(self, attr): # currently this can only be _offset_map # TODO: make this a simple sorted offset array which can be bisected diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 6e31363..24e2a31 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -217,10 +217,11 @@ def rewind_streams(): assert os.path.getsize(ppath) > 100 # verify pack - pf = PackFile(ppath) # FIXME: Leaks file-pointer(s)! + pf = PackFile(ppath) assert pf.size() == len(pack_objs) assert pf.version() == PackFile.pack_version_default assert pf.checksum() == pack_sha + pf.close() # verify index if ipath is not None: @@ -231,6 +232,7 @@ def rewind_streams(): assert idx.packfile_checksum() == pack_sha assert idx.indexfile_checksum() == index_sha assert idx.size() == len(pack_objs) + idx.close() # END verify files exist # END for each packpath, indexpath pair @@ -245,7 +247,8 @@ def rewind_streams(): # END for each crc mode # END for each info assert count == len(pack_objs) - + entity.close() + def test_pack_64(self): # TODO: hex-edit a pack helping us to verify that we can handle 64 byte offsets # of course without really needing such a huge pack From 891ac5126540dcb087242f9bb0cadd02ca021324 Mon Sep 17 00:00:00 2001 From: stuertz Date: Mon, 27 Mar 2017 11:36:43 +0200 Subject: [PATCH 045/158] Use range instead of xrange, good enough here --- gitdb/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitdb/util.py b/gitdb/util.py index 95ab9b2..8a1819b 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -72,7 +72,7 @@ def _retry(func, *args, **kwargs): # Wrapper around functions, that are problematic on "Windows". Sometimes # the OS or someone else has still a handle to the file if sys.platform == "win32": - for _ in xrange(10): + for _ in range(10): try: return func(*args, **kwargs) except Exception: From bcdffc46e3993d7743b90982009eec49df667ab4 Mon Sep 17 00:00:00 2001 From: stuertz Date: Mon, 27 Mar 2017 11:44:14 +0200 Subject: [PATCH 046/158] fixed to be py26 compat --- gitdb/test/db/test_pack.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index e6c2032..f6e2751 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -16,14 +16,16 @@ import random import sys -from unittest import skipIf +from nose.plugins.skip import SkipTest class TestPackDB(TestDBBase): - @skipIf(sys.platform == "win32", "not supported on windows currently") @with_rw_directory @with_packs_rw def test_writing(self, path): + if sys.platform == "win32": + raise SkipTest("FIXME: Currently fail on windows") + pdb = PackedDB(path) # on demand, we init our pack cache From 7bced788880015075754ce3645cef3a351166ff4 Mon Sep 17 00:00:00 2001 From: stuertz Date: Mon, 27 Mar 2017 11:46:23 +0200 Subject: [PATCH 047/158] Typos --- gitdb/test/db/test_pack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index f6e2751..9694238 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -37,9 +37,9 @@ def test_writing(self, path): pack_path = pdb.entities()[0].pack().path() new_pack_path = pack_path + "renamed" if sys.platform == "win32": - # This is just the beginning: While using thsi function, we are not - # allowed to have any handle to thsi path, which is currently not - # the case. The pack caching does have a handle :-( + # While using this function, we are not allowed to have any handle + # to this path, which is currently not the case. The pack caching + # does still have a handle :-( mman.force_map_handle_removal_win(pack_path) os.rename(pack_path, new_pack_path) From 3add9cea092514a82931108bbc05a15355900a52 Mon Sep 17 00:00:00 2001 From: wangweichen Date: Thu, 13 Jul 2017 13:55:49 +0800 Subject: [PATCH 048/158] fix open encoding error. --- gitdb/db/ref.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index 2e3db86..84f9f6c 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -41,7 +41,7 @@ def _update_dbs_from_ref_file(self): # try to get as many as possible, don't fail if some are unavailable ref_paths = list() try: - with open(self._ref_file, 'r') as f: + with open(self._ref_file, 'r', encoding="utf-8") as f: ref_paths = [l.strip() for l in f] except (OSError, IOError): pass From 5e0fea5f6b9e47f52d045d0449dd1a09943496a0 Mon Sep 17 00:00:00 2001 From: wangweichen Date: Thu, 13 Jul 2017 14:34:09 +0800 Subject: [PATCH 049/158] change codecs to open with support encoding="utf-8" --- gitdb/db/ref.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index 84f9f6c..94a2f01 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -2,6 +2,7 @@ # # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php +import codecs from gitdb.db.base import ( CompoundDB, ) @@ -41,7 +42,7 @@ def _update_dbs_from_ref_file(self): # try to get as many as possible, don't fail if some are unavailable ref_paths = list() try: - with open(self._ref_file, 'r', encoding="utf-8") as f: + with codecs.open(self._ref_file, 'r', encoding="utf-8") as f: ref_paths = [l.strip() for l in f] except (OSError, IOError): pass From 0d5062eac9d03ea63975446439600e63feb83163 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 28 Sep 2017 10:52:51 +0200 Subject: [PATCH 050/158] Upgrade makefile --- Makefile | 16 +++++++++++++++- gitdb/__init__.py | 2 +- gitdb/ext/smmap | 2 +- setup.py | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index c6c159b..8cb323e 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,21 @@ SETUP = $(PYTHON) setup.py TESTRUNNER = $(shell which nosetests) TESTFLAGS = -all: build +all:: + @grep -Ee '^[a-z].*:' Makefile | cut -d: -f1 | grep -vF all + +release:: clean + # Check if latest tag is the current head we're releasing + echo "Latest tag = $$(git tag | sort -nr | head -n1)" + echo "HEAD SHA = $$(git rev-parse head)" + echo "Latest tag SHA = $$(git tag | sort -nr | head -n1 | xargs git rev-parse)" + @test "$$(git rev-parse head)" = "$$(git tag | sort -nr | head -n1 | xargs git rev-parse)" + make force_release + +force_release:: clean + git push --tags + python3 setup.py sdist bdist_wheel + twine upload -s -i byronimo@gmail.com dist/* doc:: make -C doc/ html diff --git a/gitdb/__init__.py b/gitdb/__init__.py index bfa083b..e184e4b 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 0) +version_info = (2, 0, 3) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index ac5df70..91d506e 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit ac5df7061ee11232346b3d0eb3aa5b43eebc847d +Subproject commit 91d506e120d4a0f98cbef202325e301c632445c5 diff --git a/setup.py b/setup.py index f7e3761..4bff11d 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 0) +version_info = (2, 0, 3) __version__ = '.'.join(str(i) for i in version_info) setup( From 90c4f25493b918ff9dc4ee52ae8216a554bb3446 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 29 Sep 2017 13:16:49 +0200 Subject: [PATCH 051/158] Add python 3.6 to the testing suite --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ba0beaa..7288a2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.3" - "3.4" - "3.5" + - "3.6" # - "pypy" - won't work as smmap doesn't work (see smmap/.travis.yml for details) git: From a4ca69e9c93d00185dc729e68f2ef86ffe138410 Mon Sep 17 00:00:00 2001 From: Michael Overmeyer Date: Mon, 5 Mar 2018 20:06:45 +0000 Subject: [PATCH 052/158] Switched broken pypip.in badges to shields.io --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index ca51dfa..917b403 100644 --- a/README.rst +++ b/README.rst @@ -6,10 +6,10 @@ GitDB allows you to access bare git repositories for reading and writing. It aim Installation ============ -.. image:: https://pypip.in/version/gitdb/badge.svg +.. image:: https://img.shields.io/pypi/v/gitdb.svg :target: https://pypi.python.org/pypi/gitdb/ :alt: Latest Version -.. image:: https://pypip.in/py_versions/gitdb/badge.svg +.. image:: https://img.shields.io/pypi/pyversions/gitdb.svg :target: https://pypi.python.org/pypi/gitdb/ :alt: Supported Python versions .. image:: https://readthedocs.org/projects/gitdb/badge/?version=latest From 857196a7312209eed5c84fca857ab6c486bcbd6b Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 7 Sep 2018 21:29:54 +0300 Subject: [PATCH 053/158] Drop support for EOL Python --- .travis.yml | 2 -- gitdb/util.py | 5 +---- setup.py | 4 +--- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7288a2a..1341a1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - - "2.6" - "2.7" - - "3.3" - "3.4" - "3.5" - "3.6" diff --git a/gitdb/util.py b/gitdb/util.py index 8a1819b..d680f97 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -19,10 +19,7 @@ # initialize our global memory manager instance # Use it to free cached (and unused) resources. -if sys.version_info < (2, 6): - mman = StaticWindowMapManager() -else: - mman = SlidingWindowMapManager() +mman = SlidingWindowMapManager() # END handle mman import hashlib diff --git a/setup.py b/setup.py index 4bff11d..9315964 100755 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ zip_safe=False, install_requires=['smmap2 >= 2.0.0'], long_description="""GitDB is a pure-Python git object database""", + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ "Development Status :: 5 - Production/Stable", @@ -34,11 +35,8 @@ "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", ] From 140104a7ecb3dfb5c43b57c4829b9d7142b4aeaf Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 7 Sep 2018 21:32:56 +0300 Subject: [PATCH 054/158] Upgrade Python syntax with pyupgrade https://github.com/asottile/pyupgrade --- gitdb/db/pack.py | 2 +- gitdb/db/ref.py | 2 +- gitdb/test/lib.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gitdb/db/pack.py b/gitdb/db/pack.py index 6b03d83..1e37d73 100644 --- a/gitdb/db/pack.py +++ b/gitdb/db/pack.py @@ -148,7 +148,7 @@ def update_cache(self, force=False): # packs are supposed to be prefixed with pack- by git-convention # get all pack files, figure out what changed pack_files = set(glob.glob(os.path.join(self.root_path(), "pack-*.pack"))) - our_pack_files = set(item[1].pack().path() for item in self._entities) + our_pack_files = {item[1].pack().path() for item in self._entities} # new packs for pack_file in (pack_files - our_pack_files): diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index 94a2f01..2bb1de7 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -49,7 +49,7 @@ def _update_dbs_from_ref_file(self): # END handle alternates ref_paths_set = set(ref_paths) - cur_ref_paths_set = set(db.root_path() for db in self._dbs) + cur_ref_paths_set = {db.root_path() for db in self._dbs} # remove existing for path in (cur_ref_paths_set - ref_paths_set): diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index bbdd241..ab1842d 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -86,7 +86,7 @@ def wrapper(self): try: return func(self, path) except Exception: - sys.stderr.write("Test %s.%s failed, output is at %r\n" % (type(self).__name__, func.__name__, path)) + sys.stderr.write("Test {}.{} failed, output is at {!r}\n".format(type(self).__name__, func.__name__, path)) keep = True raise finally: From 186db667f703b695c2914040e98d7977f6e61272 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 7 Sep 2018 21:36:22 +0300 Subject: [PATCH 055/158] Python 3.6 is tested and supported --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9315964..27bb754 100755 --- a/setup.py +++ b/setup.py @@ -39,5 +39,6 @@ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", ] ) From 3e28e6e896543814ea312baa9ba6c6065caba797 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 13 Oct 2018 12:41:52 +0200 Subject: [PATCH 056/158] Bump patch level: remove support for old python versions --- gitdb/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index e184e4b..344c3de 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 3) +version_info = (2, 0, 4) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index 27bb754..ea6ba18 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 3) +version_info = (2, 0, 4) __version__ = '.'.join(str(i) for i in version_info) setup( From b1adf606f416f82ec69cd83cfc2b94cddc6928bd Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 13 Oct 2018 12:45:29 +0200 Subject: [PATCH 057/158] Another version bump... dunno what happened there. --- gitdb/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 344c3de..a2d2624 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 4) +version_info = (2, 0, 5) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index ea6ba18..27eb65c 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 4) +version_info = (2, 0, 5) __version__ = '.'.join(str(i) for i in version_info) setup( From 0e4b57d4511686d1aeb5479a7aa0dad3a5338d6e Mon Sep 17 00:00:00 2001 From: xarx00 Date: Fri, 5 Apr 2019 10:46:53 +0200 Subject: [PATCH 058/158] Fix for UnicodeEncodeError in git.Repo.clone_from() when path contains non-ascii characters --- gitdb/utils/encoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitdb/utils/encoding.py b/gitdb/utils/encoding.py index 4d270af..d8fd59a 100644 --- a/gitdb/utils/encoding.py +++ b/gitdb/utils/encoding.py @@ -8,7 +8,7 @@ text_type = unicode -def force_bytes(data, encoding="ascii"): +def force_bytes(data, encoding="utf-8"): if isinstance(data, bytes): return data From 79b705f061b51dc151a00729b722fbdebde59f5c Mon Sep 17 00:00:00 2001 From: Ruslan Kuprieiev Date: Wed, 25 Sep 2019 21:00:30 +0300 Subject: [PATCH 059/158] loose: rename only if needed Our user was experiencing issue [1] when using a git repository on NTFS mount running on Linux. The current check checks if we are running on Windows, but it should really check if we are on NTFS. And since checking fs type is not that trivial and not efficient, it is simpler and better to just always apply NTFS-specific logic, since it works on other filesystems as well. [1] https://github.com/iterative/dvc/issues/1880#issuecomment-483253764 --- gitdb/db/loose.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 192c524..53ade9f 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -225,16 +225,12 @@ def store(self, istream): if not isdir(obj_dir): mkdir(obj_dir) # END handle destination directory - # rename onto existing doesn't work on windows - if os.name == 'nt': - if isfile(obj_path): - remove(tmp_path) - else: - rename(tmp_path, obj_path) - # end rename only if needed + # rename onto existing doesn't work on NTFS + if isfile(obj_path): + remove(tmp_path) else: rename(tmp_path, obj_path) - # END handle win32 + # end rename only if needed # make sure its readable for all ! It started out as rw-- tmp file # but needs to be rwrr From d77bd023a61419effe77184c52ccf3e19afa6f60 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Sep 2019 13:21:35 +0200 Subject: [PATCH 060/158] Bump version --- gitdb/__init__.py | 2 +- gitdb/ext/smmap | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index e184e4b..344c3de 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 3) +version_info = (2, 0, 4) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 91d506e..a0060cf 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 91d506e120d4a0f98cbef202325e301c632445c5 +Subproject commit a0060cfdc9166bb0b3104e8015faf0689aa6daf1 diff --git a/setup.py b/setup.py index 27bb754..ea6ba18 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 3) +version_info = (2, 0, 4) __version__ = '.'.join(str(i) for i in version_info) setup( From 43e16318e9ab95f146e230afe0a7cbdc848473fe Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 28 Sep 2019 13:28:50 +0200 Subject: [PATCH 061/158] bump version again... --- gitdb/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 344c3de..4e88407 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 4) +version_info = (2, 0, 6) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index ea6ba18..6a2174f 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 4) +version_info = (2, 0, 6) __version__ = '.'.join(str(i) for i in version_info) setup( From 7729239951b5561f5bb5c2d5152ff76833b11826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubom=C3=ADr=20Sedl=C3=A1=C5=99?= Date: Wed, 8 Jan 2020 09:36:55 +0100 Subject: [PATCH 062/158] Fix deprecated calls for Python 3.9 The array methods fromstring/tostring have been deprecated since Python 3.2. Python 3.9 removes them completely. This was discovered when trying to build gitdb package for Fedora 33. https://bugzilla.redhat.com/show_bug.cgi?id=1788660 --- gitdb/pack.py | 2 +- gitdb/test/lib.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdb/pack.py b/gitdb/pack.py index 115d943..2ad2324 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -410,7 +410,7 @@ def offsets(self): if self._version == 2: # read stream to array, convert to tuple a = array.array('I') # 4 byte unsigned int, long are 8 byte on 64 bit it appears - a.fromstring(buffer(self._cursor.map(), self._pack_offset, self._pack_64_offset - self._pack_offset)) + a.frombytes(buffer(self._cursor.map(), self._pack_offset, self._pack_64_offset - self._pack_offset)) # networkbyteorder to something array likes more if sys.byteorder == 'little': diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index ab1842d..42b9ddc 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -157,7 +157,7 @@ def make_bytes(size_in_bytes, randomize=False): random.shuffle(producer) # END randomize a = array('i', producer) - return a.tostring() + return a.tobytes() def make_object(type, data): From c880f6b0550770eee559091d6276a2e2b097a83a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 15 Feb 2020 09:41:05 +0800 Subject: [PATCH 063/158] don't test python 2.7 anymore, support is dropped --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1341a1d..17d6380 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "2.7" - "3.4" - "3.5" - "3.6" From 2f9a799a6c9d125012bb09473bbfe6110f2a7391 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 15 Feb 2020 09:59:38 +0800 Subject: [PATCH 064/158] remove appveyor It is slow, it fails, and windows support seems unmaintained, besides always having been an incredible time sink. Thanks to everyone who brought GitDb to where it is right now, and I am happy to bring windows testing back if a maintainer can be found. --- .appveyor.yml | 49 ------------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 2daadaa..0000000 --- a/.appveyor.yml +++ /dev/null @@ -1,49 +0,0 @@ -# CI on Windows via appveyor -environment: - - matrix: - ## MINGW - # - - PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7" - - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4" - - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5" - - PYTHON: "C:\\Miniconda35-x64" - PYTHON_VERSION: "3.5" - IS_CONDA: "yes" - -install: - - set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH% - - ## Print configuration for debugging. - # - - | - echo %PATH% - uname -a - where python pip pip2 pip3 pip34 - python --version - python -c "import struct; print(struct.calcsize('P') * 8)" - - - IF "%IS_CONDA%"=="yes" ( - conda info -a & - conda install --yes --quiet pip - ) - - pip install nose wheel coveralls - - ## For commits performed with the default user. - - | - git config --global user.email "travis@ci.com" - git config --global user.name "Travis Runner" - - - pip install -e . - -build: false - -test_script: - - IF "%PYTHON_VERSION%"=="3.5" ( - nosetests -v --with-coverage - ) ELSE ( - nosetests -v - ) From df73d7f6874ff11be1b09f65c8dc425671bb924e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 15 Feb 2020 10:01:11 +0800 Subject: [PATCH 065/158] Release 3.0.0 --- gitdb/__init__.py | 2 +- setup.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 4e88407..c9c8279 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 6) +version_info = (3, 0, 0) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index 6a2174f..a66267e 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (2, 0, 6) +version_info = (3, 0, 0) __version__ = '.'.join(str(i) for i in version_info) setup( @@ -22,7 +22,7 @@ zip_safe=False, install_requires=['smmap2 >= 2.0.0'], long_description="""GitDB is a pure-Python git object database""", - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + python_requires='>=3.4', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ "Development Status :: 5 - Production/Stable", @@ -34,11 +34,10 @@ "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7" ] ) From e6ee8bf864c726a5461600de28d64c1f06f4e163 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 15 Feb 2020 10:03:50 +0800 Subject: [PATCH 066/158] Change package signature to the only yubikey I can use right now --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8cb323e..82c0a3b 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ release:: clean force_release:: clean git push --tags python3 setup.py sdist bdist_wheel - twine upload -s -i byronimo@gmail.com dist/* + twine upload -s -i 763629FEC8788FC35128B5F6EE029D1E5EB40300 dist/* doc:: make -C doc/ html From d6d1550a1e8dc327d5b310228a66f25a59d6ce9f Mon Sep 17 00:00:00 2001 From: Harmon Date: Sat, 15 Feb 2020 14:19:38 -0600 Subject: [PATCH 067/158] Remove badges for no longer existing Issue Stats site from README --- README.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.rst b/README.rst index 917b403..9febff0 100644 --- a/README.rst +++ b/README.rst @@ -56,10 +56,6 @@ DEVELOPMENT :target: https://ci.appveyor.com/project/ankostis/gitpython/branch/master) .. image:: https://coveralls.io/repos/gitpython-developers/gitdb/badge.png :target: https://coveralls.io/r/gitpython-developers/gitdb -.. image:: http://www.issuestats.com/github/gitpython-developers/gitdb/badge/pr - :target: http://www.issuestats.com/github/gitpython-developers/gitdb -.. image:: http://www.issuestats.com/github/gitpython-developers/gitdb/badge/issue - :target: http://www.issuestats.com/github/gitpython-developers/gitdb The library is considered mature, and not under active development. It's primary (known) use is in git-python. From 58bce6bd1051e4fd5df7c5c8123a1783cc6e9f84 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 07:06:35 -0600 Subject: [PATCH 068/158] Remove and replace compat.izip --- gitdb/fun.py | 4 ++-- gitdb/pack.py | 3 +-- gitdb/utils/compat.py | 2 -- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/gitdb/fun.py b/gitdb/fun.py index 8ca38c8..3a2248f 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -16,7 +16,7 @@ from gitdb.const import NULL_BYTE, BYTE_SPACE from gitdb.utils.encoding import force_text -from gitdb.utils.compat import izip, buffer, xrange, PY3 +from gitdb.utils.compat import buffer, xrange, PY3 from gitdb.typ import ( str_blob_type, str_commit_type, @@ -314,7 +314,7 @@ def check_integrity(self, target_size=-1): right.next() # this is very pythonic - we might have just use index based access here, # but this could actually be faster - for lft, rgt in izip(left, right): + for lft, rgt in zip(left, right): assert lft.rbound() == rgt.to assert lft.to + lft.ts == rgt.to # END for each pair diff --git a/gitdb/pack.py b/gitdb/pack.py index 2ad2324..748df38 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -63,7 +63,6 @@ from gitdb.const import NULL_BYTE from gitdb.utils.compat import ( - izip, buffer, xrange, to_bytes @@ -696,7 +695,7 @@ def _set_cache_(self, attr): iter_offsets = iter(offsets_sorted) iter_offsets_plus_one = iter(offsets_sorted) next(iter_offsets_plus_one) - consecutive = izip(iter_offsets, iter_offsets_plus_one) + consecutive = zip(iter_offsets, iter_offsets_plus_one) offset_map = dict(consecutive) diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py index a7899cb..586f3bb 100644 --- a/gitdb/utils/compat.py +++ b/gitdb/utils/compat.py @@ -3,11 +3,9 @@ PY3 = sys.version_info[0] == 3 try: - from itertools import izip xrange = xrange except ImportError: # py3 - izip = zip xrange = range # end handle python version From 73a9f7965139e319446c04bbcc9794a8db0de45a Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 07:07:51 -0600 Subject: [PATCH 069/158] Remove and replace izip in TestPack --- gitdb/test/test_pack.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 24e2a31..dd1c830 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -27,11 +27,6 @@ from gitdb.util import to_bin_sha from gitdb.utils.compat import xrange -try: - from itertools import izip -except ImportError: - izip = zip - from nose import SkipTest import os @@ -155,7 +150,7 @@ def test_pack_entity(self, rw_dir): pack_objs.extend(entity.stream_iter()) count = 0 - for info, stream in izip(entity.info_iter(), entity.stream_iter()): + for info, stream in zip(entity.info_iter(), entity.stream_iter()): count += 1 assert info.binsha == stream.binsha assert len(info.binsha) == 20 From 4a692fdd43e67810509b1c1843fa203714b14f0e Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 07:13:00 -0600 Subject: [PATCH 070/158] Remove and replace compat.xrange --- gitdb/db/pack.py | 3 +-- gitdb/fun.py | 4 ++-- gitdb/pack.py | 9 ++++----- gitdb/test/db/lib.py | 3 +-- gitdb/test/lib.py | 3 +-- gitdb/test/performance/test_pack.py | 3 +-- gitdb/test/test_pack.py | 3 +-- gitdb/utils/compat.py | 7 ------- 8 files changed, 11 insertions(+), 24 deletions(-) diff --git a/gitdb/db/pack.py b/gitdb/db/pack.py index 1e37d73..177ed7b 100644 --- a/gitdb/db/pack.py +++ b/gitdb/db/pack.py @@ -18,7 +18,6 @@ ) from gitdb.pack import PackEntity -from gitdb.utils.compat import xrange from functools import reduce @@ -107,7 +106,7 @@ def sha_iter(self): for entity in self.entities(): index = entity.index() sha_by_index = index.sha - for index in xrange(index.size()): + for index in range(index.size()): yield sha_by_index(index) # END for each index # END for each entity diff --git a/gitdb/fun.py b/gitdb/fun.py index 3a2248f..7203de9 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -16,7 +16,7 @@ from gitdb.const import NULL_BYTE, BYTE_SPACE from gitdb.utils.encoding import force_text -from gitdb.utils.compat import buffer, xrange, PY3 +from gitdb.utils.compat import buffer, PY3 from gitdb.typ import ( str_blob_type, str_commit_type, @@ -264,7 +264,7 @@ def compress(self): # if first_data_index is not None: nd = StringIO() # new data so = self[first_data_index].to # start offset in target buffer - for x in xrange(first_data_index, i - 1): + for x in range(first_data_index, i - 1): xdc = self[x] nd.write(xdc.data[:xdc.ts]) # END collect data diff --git a/gitdb/pack.py b/gitdb/pack.py index 748df38..f010554 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -64,7 +64,6 @@ from gitdb.const import NULL_BYTE from gitdb.utils.compat import ( buffer, - xrange, to_bytes ) @@ -206,7 +205,7 @@ def write(self, pack_sha, write): for t in self._objs: tmplist[byte_ord(t[0][0])] += 1 # END prepare fanout - for i in xrange(255): + for i in range(255): v = tmplist[i] sha_write(pack('>L', v)) tmplist[i + 1] += v @@ -375,7 +374,7 @@ def _read_fanout(self, byte_offset): d = self._cursor.map() out = list() append = out.append - for i in xrange(256): + for i in range(256): append(unpack_from('>L', d, byte_offset + i * 4)[0]) # END for each entry return out @@ -416,7 +415,7 @@ def offsets(self): a.byteswap() return a else: - return tuple(self.offset(index) for index in xrange(self.size())) + return tuple(self.offset(index) for index in range(self.size())) # END handle version def sha_to_index(self, sha): @@ -715,7 +714,7 @@ def _iter_objects(self, as_stream): """Iterate over all objects in our index and yield their OInfo or OStream instences""" _sha = self._index.sha _object = self._object - for index in xrange(self._index.size()): + for index in range(self._index.size()): yield _object(_sha(index), as_stream, index) # END for each index diff --git a/gitdb/test/db/lib.py b/gitdb/test/db/lib.py index 528bcc1..c6f4316 100644 --- a/gitdb/test/db/lib.py +++ b/gitdb/test/db/lib.py @@ -23,7 +23,6 @@ from gitdb.exc import BadObject from gitdb.typ import str_blob_type -from gitdb.utils.compat import xrange from io import BytesIO @@ -45,7 +44,7 @@ def _assert_object_writing_simple(self, db): # write a bunch of objects and query their streams and info null_objs = db.size() ni = 250 - for i in xrange(ni): + for i in range(ni): data = pack(">L", i) istream = IStream(str_blob_type, len(data), BytesIO(data)) new_istream = db.store(istream) diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index 42b9ddc..a04084f 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -4,7 +4,6 @@ # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Utilities used in ODB testing""" from gitdb import OStream -from gitdb.utils.compat import xrange import sys import random @@ -151,7 +150,7 @@ def make_bytes(size_in_bytes, randomize=False): """:return: string with given size in bytes :param randomize: try to produce a very random stream""" actual_size = size_in_bytes // 4 - producer = xrange(actual_size) + producer = range(actual_size) if randomize: producer = list(producer) random.shuffle(producer) diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index fc8d9d5..b59d5a9 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -17,7 +17,6 @@ from gitdb.typ import str_blob_type from gitdb.exc import UnsupportedOperation from gitdb.db.pack import PackedDB -from gitdb.utils.compat import xrange from gitdb.test.lib import skip_on_travis_ci import sys @@ -118,7 +117,7 @@ def test_correctness(self): for entity in pdb.entities(): pack_verify = entity.is_valid_stream sha_by_index = entity.index().sha - for index in xrange(entity.index().size()): + for index in range(entity.index().size()): try: assert pack_verify(sha_by_index(index), use_crc=crc) count += 1 diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index dd1c830..8bf78f0 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -25,7 +25,6 @@ from gitdb.fun import delta_types from gitdb.exc import UnsupportedOperation from gitdb.util import to_bin_sha -from gitdb.utils.compat import xrange from nose import SkipTest @@ -58,7 +57,7 @@ def _assert_index_file(self, index, version, size): assert len(index.offsets()) == size # get all data of all objects - for oidx in xrange(index.size()): + for oidx in range(index.size()): sha = index.sha(oidx) assert oidx == index.sha_to_index(sha) diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py index 586f3bb..99e7ae6 100644 --- a/gitdb/utils/compat.py +++ b/gitdb/utils/compat.py @@ -2,13 +2,6 @@ PY3 = sys.version_info[0] == 3 -try: - xrange = xrange -except ImportError: - # py3 - xrange = range -# end handle python version - try: # Python 2 buffer = buffer From 3d14cc84b0a5e42d755d24d77a6e4af6bea8c3c1 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 07:36:03 -0600 Subject: [PATCH 071/158] Remove and replace compat.buffer --- gitdb/fun.py | 8 ++++---- gitdb/pack.py | 11 ++++------- gitdb/stream.py | 5 ++--- gitdb/utils/compat.py | 10 ---------- 4 files changed, 10 insertions(+), 24 deletions(-) diff --git a/gitdb/fun.py b/gitdb/fun.py index 7203de9..92b8b1c 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -16,7 +16,7 @@ from gitdb.const import NULL_BYTE, BYTE_SPACE from gitdb.utils.encoding import force_text -from gitdb.utils.compat import buffer, PY3 +from gitdb.utils.compat import PY3 from gitdb.typ import ( str_blob_type, str_commit_type, @@ -101,7 +101,7 @@ def delta_chunk_apply(dc, bbuf, write): :param write: write method to call with data to write""" if dc.data is None: # COPY DATA FROM SOURCE - write(buffer(bbuf, dc.so, dc.ts)) + write(bbuf[dc.so:dc.so + dc.ts]) else: # APPEND DATA # whats faster: if + 4 function calls or just a write with a slice ? @@ -698,7 +698,7 @@ def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): if (rbound < cp_size or rbound > src_buf_size): break - write(buffer(src_buf, cp_off, cp_size)) + write(src_buf[cp_off:cp_off + cp_size]) elif c: write(db[i:i + c]) i += c @@ -741,7 +741,7 @@ def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): if (rbound < cp_size or rbound > src_buf_size): break - write(buffer(src_buf, cp_off, cp_size)) + write(src_buf[cp_off:cp_off + cp_size]) elif c: write(db[i:i + c]) i += c diff --git a/gitdb/pack.py b/gitdb/pack.py index f010554..68da2b7 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -62,10 +62,7 @@ from binascii import crc32 from gitdb.const import NULL_BYTE -from gitdb.utils.compat import ( - buffer, - to_bytes -) +from gitdb.utils.compat import to_bytes import tempfile import array @@ -117,7 +114,7 @@ def pack_object_at(cursor, offset, as_stream): # END handle type id abs_data_offset = offset + total_rela_offset if as_stream: - stream = DecompressMemMapReader(buffer(data, total_rela_offset), False, uncomp_size) + stream = DecompressMemMapReader(data[total_rela_offset:], False, uncomp_size) if delta_info is None: return abs_data_offset, OPackStream(offset, type_id, uncomp_size, stream) else: @@ -408,7 +405,7 @@ def offsets(self): if self._version == 2: # read stream to array, convert to tuple a = array.array('I') # 4 byte unsigned int, long are 8 byte on 64 bit it appears - a.frombytes(buffer(self._cursor.map(), self._pack_offset, self._pack_64_offset - self._pack_offset)) + a.frombytes(self._cursor.map()[self._pack_offset:self._pack_64_offset]) # networkbyteorder to something array likes more if sys.byteorder == 'little': @@ -836,7 +833,7 @@ def is_valid_stream(self, sha, use_crc=False): while cur_pos < next_offset: rbound = min(cur_pos + chunk_size, next_offset) size = rbound - cur_pos - this_crc_value = crc_update(buffer(pack_data, cur_pos, size), this_crc_value) + this_crc_value = crc_update(pack_data[cur_pos:cur_pos + size], this_crc_value) cur_pos += size # END window size loop diff --git a/gitdb/stream.py b/gitdb/stream.py index 2f4c12d..b94ef24 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -27,7 +27,6 @@ ) from gitdb.const import NULL_BYTE, BYTE_SPACE -from gitdb.utils.compat import buffer from gitdb.utils.encoding import force_bytes has_perf_mod = False @@ -278,7 +277,7 @@ def read(self, size=-1): # END adjust winsize # takes a slice, but doesn't copy the data, it says ... - indata = buffer(self._m, self._cws, self._cwe - self._cws) + indata = self._m[self._cws:self._cwe] # get the actual window end to be sure we don't use it for computations self._cwe = self._cws + len(indata) @@ -414,7 +413,7 @@ def _set_cache_brute_(self, attr): buf = dstream.read(512) # read the header information + X offset, src_size = msb_size(buf) offset, target_size = msb_size(buf, offset) - buffer_info_list.append((buffer(buf, offset), offset, src_size, target_size)) + buffer_info_list.append((buf[offset:], offset, src_size, target_size)) max_target_size = max(max_target_size, target_size) # END for each delta stream diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py index 99e7ae6..8c7c06b 100644 --- a/gitdb/utils/compat.py +++ b/gitdb/utils/compat.py @@ -4,22 +4,12 @@ try: # Python 2 - buffer = buffer memoryview = buffer # Assume no memory view ... def to_bytes(i): return i except NameError: # Python 3 has no `buffer`; only `memoryview` - # However, it's faster to just slice the object directly, maybe it keeps a view internally - def buffer(obj, offset, size=None): - if size is None: - # return memoryview(obj)[offset:] - return obj[offset:] - else: - # return memoryview(obj)[offset:offset+size] - return obj[offset:offset + size] - # end buffer reimplementation # smmap can return memory view objects, which can't be compared as buffers/bytes can ... def to_bytes(i): if isinstance(i, memoryview): From d1c20d559a1f264dad12d1033a052ffc1c159260 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 07:37:34 -0600 Subject: [PATCH 072/158] Remove compat.memoryview --- gitdb/utils/compat.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py index 8c7c06b..d5791ed 100644 --- a/gitdb/utils/compat.py +++ b/gitdb/utils/compat.py @@ -4,20 +4,15 @@ try: # Python 2 - memoryview = buffer - # Assume no memory view ... def to_bytes(i): return i except NameError: - # Python 3 has no `buffer`; only `memoryview` # smmap can return memory view objects, which can't be compared as buffers/bytes can ... def to_bytes(i): if isinstance(i, memoryview): return i.tobytes() return i - memoryview = memoryview - try: MAXSIZE = sys.maxint except AttributeError: From 59c053ee05d6e5f50f8699260aa0e362b567c033 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 07:54:24 -0600 Subject: [PATCH 073/158] Remove and replace compat.to_bytes --- gitdb/pack.py | 7 +++++-- gitdb/utils/compat.py | 11 ----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/gitdb/pack.py b/gitdb/pack.py index 68da2b7..a38468e 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -62,7 +62,6 @@ from binascii import crc32 from gitdb.const import NULL_BYTE -from gitdb.utils.compat import to_bytes import tempfile import array @@ -877,7 +876,11 @@ def collect_streams_at_offset(self, offset): stream = streams[-1] while stream.type_id in delta_types: if stream.type_id == REF_DELTA: - sindex = self._index.sha_to_index(to_bytes(stream.delta_info)) + # smmap can return memory view objects, which can't be compared as buffers/bytes can ... + if isinstance(stream.delta_info, memoryview): + sindex = self._index.sha_to_index(stream.delta_info.tobytes()) + else: + sindex = self._index.sha_to_index(stream.delta_info) if sindex is None: break stream = self._pack.stream(self._index.offset(sindex)) diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py index d5791ed..6909c53 100644 --- a/gitdb/utils/compat.py +++ b/gitdb/utils/compat.py @@ -2,17 +2,6 @@ PY3 = sys.version_info[0] == 3 -try: - # Python 2 - def to_bytes(i): - return i -except NameError: - # smmap can return memory view objects, which can't be compared as buffers/bytes can ... - def to_bytes(i): - if isinstance(i, memoryview): - return i.tobytes() - return i - try: MAXSIZE = sys.maxint except AttributeError: From 321c3b46b4792cf83bf0c5814d5a1a43cdf6933d Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 07:57:23 -0600 Subject: [PATCH 074/158] Remove and replace compat.MAXSIZE --- gitdb/db/loose.py | 4 ++-- gitdb/utils/compat.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 53ade9f..7bf92da 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -50,11 +50,11 @@ stream_copy ) -from gitdb.utils.compat import MAXSIZE from gitdb.utils.encoding import force_bytes import tempfile import os +import sys __all__ = ('LooseObjectDB', ) @@ -196,7 +196,7 @@ def store(self, istream): if istream.binsha is not None: # copy as much as possible, the actual uncompressed item size might # be smaller than the compressed version - stream_copy(istream.read, writer.write, MAXSIZE, self.stream_chunk_size) + stream_copy(istream.read, writer.write, sys.maxsize, self.stream_chunk_size) else: # write object with header, we have to make a new one write_object(istream.type, istream.size, istream.read, writer.write, diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py index 6909c53..c426474 100644 --- a/gitdb/utils/compat.py +++ b/gitdb/utils/compat.py @@ -1,8 +1,3 @@ import sys PY3 = sys.version_info[0] == 3 - -try: - MAXSIZE = sys.maxint -except AttributeError: - MAXSIZE = sys.maxsize From c41268071bba593cecd420c400603e094f40a6dc Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 08:01:00 -0600 Subject: [PATCH 075/158] Remove and replace encoding.string_types --- gitdb/utils/encoding.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gitdb/utils/encoding.py b/gitdb/utils/encoding.py index 4d270af..156f016 100644 --- a/gitdb/utils/encoding.py +++ b/gitdb/utils/encoding.py @@ -1,10 +1,8 @@ from gitdb.utils import compat if compat.PY3: - string_types = (str, ) text_type = str else: - string_types = (basestring, ) text_type = unicode @@ -12,7 +10,7 @@ def force_bytes(data, encoding="ascii"): if isinstance(data, bytes): return data - if isinstance(data, string_types): + if isinstance(data, str): return data.encode(encoding) return data From 77dc809542d15c40dbe60ff55cd830082c3ad904 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 08:02:08 -0600 Subject: [PATCH 076/158] Remove and replace encoding.text_type --- gitdb/utils/encoding.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/gitdb/utils/encoding.py b/gitdb/utils/encoding.py index 156f016..25ebead 100644 --- a/gitdb/utils/encoding.py +++ b/gitdb/utils/encoding.py @@ -1,11 +1,3 @@ -from gitdb.utils import compat - -if compat.PY3: - text_type = str -else: - text_type = unicode - - def force_bytes(data, encoding="ascii"): if isinstance(data, bytes): return data @@ -17,13 +9,10 @@ def force_bytes(data, encoding="ascii"): def force_text(data, encoding="utf-8"): - if isinstance(data, text_type): + if isinstance(data, str): return data if isinstance(data, bytes): return data.decode(encoding) - if compat.PY3: - return text_type(data, encoding) - else: - return text_type(data) + return str(data, encoding) From db9a65e3b0eceb9c52359273afb3313860e5e322 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 16 Feb 2020 08:11:54 -0600 Subject: [PATCH 077/158] Remove compat.PY3 --- gitdb/fun.py | 211 ++++++++++++++---------------------------- gitdb/utils/compat.py | 3 - 2 files changed, 67 insertions(+), 147 deletions(-) delete mode 100644 gitdb/utils/compat.py diff --git a/gitdb/fun.py b/gitdb/fun.py index 92b8b1c..9846597 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -16,7 +16,6 @@ from gitdb.const import NULL_BYTE, BYTE_SPACE from gitdb.utils.encoding import force_text -from gitdb.utils.compat import PY3 from gitdb.typ import ( str_blob_type, str_commit_type, @@ -424,20 +423,12 @@ def pack_object_header_info(data): type_id = (c >> 4) & 7 # numeric type size = c & 15 # starting size s = 4 # starting bit-shift size - if PY3: - while c & 0x80: - c = byte_ord(data[i]) - i += 1 - size += (c & 0x7f) << s - s += 7 - # END character loop - else: - while c & 0x80: - c = ord(data[i]) - i += 1 - size += (c & 0x7f) << s - s += 7 - # END character loop + while c & 0x80: + c = byte_ord(data[i]) + i += 1 + size += (c & 0x7f) << s + s += 7 + # END character loop # end performance at expense of maintenance ... return (type_id, size, i) @@ -450,28 +441,16 @@ def create_pack_object_header(obj_type, obj_size): :param obj_type: pack type_id of the object :param obj_size: uncompressed size in bytes of the following object stream""" c = 0 # 1 byte - if PY3: - hdr = bytearray() # output string - - c = (obj_type << 4) | (obj_size & 0xf) - obj_size >>= 4 - while obj_size: - hdr.append(c | 0x80) - c = obj_size & 0x7f - obj_size >>= 7 - # END until size is consumed - hdr.append(c) - else: - hdr = bytes() # output string - - c = (obj_type << 4) | (obj_size & 0xf) - obj_size >>= 4 - while obj_size: - hdr += chr(c | 0x80) - c = obj_size & 0x7f - obj_size >>= 7 - # END until size is consumed - hdr += chr(c) + hdr = bytearray() # output string + + c = (obj_type << 4) | (obj_size & 0xf) + obj_size >>= 4 + while obj_size: + hdr.append(c | 0x80) + c = obj_size & 0x7f + obj_size >>= 7 + # END until size is consumed + hdr.append(c) # end handle interpreter return hdr @@ -484,26 +463,15 @@ def msb_size(data, offset=0): i = 0 l = len(data) hit_msb = False - if PY3: - while i < l: - c = data[i + offset] - size |= (c & 0x7f) << i * 7 - i += 1 - if not c & 0x80: - hit_msb = True - break - # END check msb bit - # END while in range - else: - while i < l: - c = ord(data[i + offset]) - size |= (c & 0x7f) << i * 7 - i += 1 - if not c & 0x80: - hit_msb = True - break - # END check msb bit - # END while in range + while i < l: + c = data[i + offset] + size |= (c & 0x7f) << i * 7 + i += 1 + if not c & 0x80: + hit_msb = True + break + # END check msb bit + # END while in range # end performance ... if not hit_msb: raise AssertionError("Could not find terminating MSB byte in data stream") @@ -663,93 +631,48 @@ def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): **Note:** transcribed to python from the similar routine in patch-delta.c""" i = 0 db = delta_buf - if PY3: - while i < delta_buf_size: - c = db[i] - i += 1 - if c & 0x80: - cp_off, cp_size = 0, 0 - if (c & 0x01): - cp_off = db[i] - i += 1 - if (c & 0x02): - cp_off |= (db[i] << 8) - i += 1 - if (c & 0x04): - cp_off |= (db[i] << 16) - i += 1 - if (c & 0x08): - cp_off |= (db[i] << 24) - i += 1 - if (c & 0x10): - cp_size = db[i] - i += 1 - if (c & 0x20): - cp_size |= (db[i] << 8) - i += 1 - if (c & 0x40): - cp_size |= (db[i] << 16) - i += 1 - - if not cp_size: - cp_size = 0x10000 - - rbound = cp_off + cp_size - if (rbound < cp_size or - rbound > src_buf_size): - break - write(src_buf[cp_off:cp_off + cp_size]) - elif c: - write(db[i:i + c]) - i += c - else: - raise ValueError("unexpected delta opcode 0") - # END handle command byte - # END while processing delta data - else: - while i < delta_buf_size: - c = ord(db[i]) - i += 1 - if c & 0x80: - cp_off, cp_size = 0, 0 - if (c & 0x01): - cp_off = ord(db[i]) - i += 1 - if (c & 0x02): - cp_off |= (ord(db[i]) << 8) - i += 1 - if (c & 0x04): - cp_off |= (ord(db[i]) << 16) - i += 1 - if (c & 0x08): - cp_off |= (ord(db[i]) << 24) - i += 1 - if (c & 0x10): - cp_size = ord(db[i]) - i += 1 - if (c & 0x20): - cp_size |= (ord(db[i]) << 8) - i += 1 - if (c & 0x40): - cp_size |= (ord(db[i]) << 16) - i += 1 - - if not cp_size: - cp_size = 0x10000 - - rbound = cp_off + cp_size - if (rbound < cp_size or - rbound > src_buf_size): - break - write(src_buf[cp_off:cp_off + cp_size]) - elif c: - write(db[i:i + c]) - i += c - else: - raise ValueError("unexpected delta opcode 0") - # END handle command byte - # END while processing delta data - # end save byte_ord call and prevent performance regression in py2 + while i < delta_buf_size: + c = db[i] + i += 1 + if c & 0x80: + cp_off, cp_size = 0, 0 + if (c & 0x01): + cp_off = db[i] + i += 1 + if (c & 0x02): + cp_off |= (db[i] << 8) + i += 1 + if (c & 0x04): + cp_off |= (db[i] << 16) + i += 1 + if (c & 0x08): + cp_off |= (db[i] << 24) + i += 1 + if (c & 0x10): + cp_size = db[i] + i += 1 + if (c & 0x20): + cp_size |= (db[i] << 8) + i += 1 + if (c & 0x40): + cp_size |= (db[i] << 16) + i += 1 + + if not cp_size: + cp_size = 0x10000 + + rbound = cp_off + cp_size + if (rbound < cp_size or + rbound > src_buf_size): + break + write(src_buf[cp_off:cp_off + cp_size]) + elif c: + write(db[i:i + c]) + i += c + else: + raise ValueError("unexpected delta opcode 0") + # END handle command byte + # END while processing delta data # yes, lets use the exact same error message that git uses :) assert i == delta_buf_size, "delta replay has gone wild" diff --git a/gitdb/utils/compat.py b/gitdb/utils/compat.py deleted file mode 100644 index c426474..0000000 --- a/gitdb/utils/compat.py +++ /dev/null @@ -1,3 +0,0 @@ -import sys - -PY3 = sys.version_info[0] == 3 From 5dd0f302f101e66d9d70a3b17ce0f379b4db214b Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 17 Feb 2020 09:15:48 +0800 Subject: [PATCH 078/158] bump version to 3.0.1 --- doc/source/changes.rst | 6 ++++++ gitdb/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 22deb6d..9d3b2dc 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ######### +***** +3.0.1 +***** + +* removed all python2 compatibility shims, GitDB now is a Python 3 program. + ***** 0.6.1 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index c9c8279..6fa31e4 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (3, 0, 0) +version_info = (3, 0, 1) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index a66267e..12cebcd 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (3, 0, 0) +version_info = (3, 0, 1) __version__ = '.'.join(str(i) for i in version_info) setup( From f356e12766480852d0e30ae7b786cdf5f24d8cea Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 17 Feb 2020 11:16:43 +0800 Subject: [PATCH 079/158] Now with PR: 3.0.2 --- doc/source/changes.rst | 2 +- gitdb/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 9d3b2dc..aa7a890 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -3,7 +3,7 @@ Changelog ######### ***** -3.0.1 +3.0.2 ***** * removed all python2 compatibility shims, GitDB now is a Python 3 program. diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 6fa31e4..5a52cf2 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (3, 0, 1) +version_info = (3, 0, 2) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index 12cebcd..a66b229 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (3, 0, 1) +version_info = (3, 0, 2) __version__ = '.'.join(str(i) for i in version_info) setup( From 02de02cbd938015ff6ba3e668da4e641fdd74c4a Mon Sep 17 00:00:00 2001 From: Harmon Date: Sat, 22 Feb 2020 16:25:56 -0600 Subject: [PATCH 080/158] Restrict smmap2 version to <3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a66b229..49cbf36 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ packages=('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), license="BSD License", zip_safe=False, - install_requires=['smmap2 >= 2.0.0'], + install_requires=['smmap2>=2,<3'], long_description="""GitDB is a pure-Python git object database""", python_requires='>=3.4', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers From 139811a89279482c4df9cddb7d7e69d2e2c36c47 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sat, 22 Feb 2020 16:26:11 -0600 Subject: [PATCH 081/158] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed4898e..92b8872 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -smmap>=0.8.3 +smmap2>=2,<3 From 09aa35b62ed341124a7b4757acf35b849a7a39ad Mon Sep 17 00:00:00 2001 From: Harmon Date: Sat, 22 Feb 2020 18:15:58 -0600 Subject: [PATCH 082/158] Improve changelog for v3.0.2 --- doc/source/changes.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index aa7a890..e4d4ebc 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -6,7 +6,8 @@ Changelog 3.0.2 ***** -* removed all python2 compatibility shims, GitDB now is a Python 3 program. +* Removed Python 2 compatibility shims + (`#56 `_) ***** 0.6.1 From 74aef73b0dd1f97b89f95f67500ae9c4c405ff15 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 23 Feb 2020 00:00:54 -0600 Subject: [PATCH 083/158] v3.0.3.post1 --- doc/source/changes.rst | 15 +++++++++++++++ gitdb/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index e4d4ebc..ac4b477 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,21 @@ Changelog ######### +*********** +3.0.3.post1 +*********** + +* Fixed changelogs for v3.0.2 and v3.0.3 + +***** +3.0.3 +***** + +* Changed ``force_bytes`` to use UTF-8 encoding by default + (`#49 `_) +* Restricted smmap2 version requirement to < 3 +* Updated requirements.txt + ***** 3.0.2 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 5a52cf2..b6a0341 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (3, 0, 2) +version_info = (3, 0, 3, "post1") __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index 49cbf36..b79dcb2 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (3, 0, 2) +version_info = (3, 0, 3, "post1") __version__ = '.'.join(str(i) for i in version_info) setup( From 253dfe7092f83229d9e99059e7c51f678a557fd2 Mon Sep 17 00:00:00 2001 From: Harmon Date: Sun, 23 Feb 2020 09:13:36 -0600 Subject: [PATCH 084/158] v4.0.1 --- doc/source/changes.rst | 10 ++++++++++ gitdb/__init__.py | 2 +- gitdb/ext/smmap | 2 +- requirements.txt | 2 +- setup.py | 6 +++--- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index ac4b477..236b954 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,16 @@ Changelog ######### +***** +4.0.1 +***** + +* Switched back to the gitdb package name on PyPI and fixed the gitdb2 mirror package + (`#59 `_) +* Switched back to require smmap package and fixed version requirement to >= 3.0.1, < 4 + (`#59 `_) +* Updated smmap submodule + *********** 3.0.3.post1 *********** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index b6a0341..5d68ccb 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (3, 0, 3, "post1") +version_info = (4, 0, 1) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index a0060cf..d076f66 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit a0060cfdc9166bb0b3104e8015faf0689aa6daf1 +Subproject commit d076f665dae16bd03eeb9df862860d54968eda84 diff --git a/requirements.txt b/requirements.txt index 92b8872..b6ccf50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -smmap2>=2,<3 +smmap>=3.0.1,<4 diff --git a/setup.py b/setup.py index b79dcb2..7617583 100755 --- a/setup.py +++ b/setup.py @@ -7,11 +7,11 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (3, 0, 3, "post1") +version_info = (4, 0, 1) __version__ = '.'.join(str(i) for i in version_info) setup( - name="gitdb2", + name="gitdb", version=__version__, description="Git Object Database", author=__author__, @@ -20,7 +20,7 @@ packages=('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), license="BSD License", zip_safe=False, - install_requires=['smmap2>=2,<3'], + install_requires=['smmap>=3.0.1,<4'], long_description="""GitDB is a pure-Python git object database""", python_requires='>=3.4', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers From 02d83a8bad286eba61de54616f1df183825800de Mon Sep 17 00:00:00 2001 From: Nicusor97 Date: Mon, 24 Feb 2020 17:56:18 +0200 Subject: [PATCH 085/158] Remove setup.cfg because the gitdb dropped Python 2 support --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79c..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 From 9b0309553be36cd188b3fd7d2ab95e7497a5e80d Mon Sep 17 00:00:00 2001 From: Harmon Date: Mon, 24 Feb 2020 10:45:10 -0600 Subject: [PATCH 086/158] v4.0.2 --- doc/source/changes.rst | 7 +++++++ gitdb/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 236b954..1e91121 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,13 @@ Changelog ######### +***** +4.0.2 +***** + +* Updated to release as Pure Python Wheel rather than Universal Wheel + (`#62 `_) + ***** 4.0.1 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 5d68ccb..df1f242 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -29,7 +29,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 1) +version_info = (4, 0, 2) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index 7617583..d110099 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 1) +version_info = (4, 0, 2) __version__ = '.'.join(str(i) for i in version_info) setup( From 77b39e915ef32ca83fb61276128e41e5d7886dc2 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 13:57:34 +0800 Subject: [PATCH 087/158] Create pythonpackage.yml --- .github/workflows/pythonpackage.yml | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/pythonpackage.yml diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml new file mode 100644 index 0000000..4c67fbf --- /dev/null +++ b/.github/workflows/pythonpackage.yml @@ -0,0 +1,44 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 1000 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Lint with flake8 + run: | + pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with nose + run: | + pip install nose + ulimit -n 48 + ulimit -n + nosetests -v From cc21afae16d00291ea97cc1f159a4c268b94a198 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 14:08:42 +0800 Subject: [PATCH 088/158] Replace travis with github actions --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9febff0..05511a7 100644 --- a/README.rst +++ b/README.rst @@ -50,8 +50,8 @@ Run the tests with DEVELOPMENT =========== -.. image:: https://travis-ci.org/gitpython-developers/gitdb.svg?branch=master - :target: https://travis-ci.org/gitpython-developers/gitdb +.. image:: https://github.com/gitpython-developers/gitdb/workflows/Python%20package/badge.svg + :target: https://github.com/gitpython-developers/gitdb/actions .. image:: https://ci.appveyor.com/api/projects/status/2qa4km4ln7bfv76r/branch/master?svg=true&passingText=windows%20OK&failingText=windows%20failed :target: https://ci.appveyor.com/project/ankostis/gitpython/branch/master) .. image:: https://coveralls.io/repos/gitpython-developers/gitdb/badge.png From e6cc702bf8fdd5e742ed77d607e7b697d27bcfef Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 14:09:05 +0800 Subject: [PATCH 089/158] don't try to use __file__ when using pyoxidizer --- gitdb/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index df1f242..2b06b24 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -13,7 +13,8 @@ def _init_externals(): """Initialize external projects by putting them into the path""" for module in ('smmap',): - sys.path.append(os.path.join(os.path.dirname(__file__), 'ext', module)) + if 'PYOXIDIZER' not in os.environ: + sys.path.append(os.path.join(os.path.dirname(__file__), 'ext', module)) try: __import__(module) From 57d3f7544820f9fd4202f4ebd0f198c43c8e575d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 14:35:01 +0800 Subject: [PATCH 090/158] bump patch level; mark travis-ci as unused --- .travis.yml | 1 + gitdb/ext/smmap | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 17d6380..b980d36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +# NOT USED, just for reference. See github actions for CI configuration language: python python: - "3.4" diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index d076f66..f4d7a58 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit d076f665dae16bd03eeb9df862860d54968eda84 +Subproject commit f4d7a58b4d96200cd057a38a0758d3c84901f57e diff --git a/setup.py b/setup.py index d110099..e8f8d68 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 2) +version_info = (4, 0, 3) __version__ = '.'.join(str(i) for i in version_info) setup( From 0002408bc03033ed90041d04c4547343a1505537 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 14:37:37 +0800 Subject: [PATCH 091/158] update changelog; remove sublime text (editor) configuration --- doc/source/changes.rst | 6 ++++ etc/sublime-text/gitdb.sublime-project | 44 -------------------------- 2 files changed, 6 insertions(+), 44 deletions(-) delete mode 100644 etc/sublime-text/gitdb.sublime-project diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 1e91121..3c116c3 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ######### +***** +4.0.3 +***** + +* Support for PyOxidizer + ***** 4.0.2 ***** diff --git a/etc/sublime-text/gitdb.sublime-project b/etc/sublime-text/gitdb.sublime-project deleted file mode 100644 index d0e2e51..0000000 --- a/etc/sublime-text/gitdb.sublime-project +++ /dev/null @@ -1,44 +0,0 @@ -{ - "folders": - [ - // GITDB - //////// - { - "follow_symlinks": true, - "path": "../..", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", - "gitdb/ext", - "dist", - "doc/build", - ".tox" - ] - }, - // SMMAP - //////// - { - "follow_symlinks": true, - "path": "../../gitdb/ext/smmap", - "file_exclude_patterns" : [ - "*.sublime-workspace", - ".git", - ".noseids", - ".coverage" - ], - "folder_exclude_patterns" : [ - ".git", - "cover", - "dist", - "doc/build", - ".tox" - ] - }, - ] -} From b8ab128b7ff8dc951ac42c1fd57d61c26864680a Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 14:40:07 +0800 Subject: [PATCH 092/158] =?UTF-8?q?Bump=20patch=20again;=20=E2=80=A6upload?= =?UTF-8?q?=20error=20due=20to=20state=20on=20disk,=20yikes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/source/changes.rst | 2 +- gitdb/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 3c116c3..69a6dba 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -3,7 +3,7 @@ Changelog ######### ***** -4.0.3 +4.0.4 ***** * Support for PyOxidizer diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 2b06b24..5f56773 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -30,7 +30,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 2) +version_info = (4, 0, 4) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index e8f8d68..48d641a 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 3) +version_info = (4, 0, 4) __version__ = '.'.join(str(i) for i in version_info) setup( From 4f08b0a2bb6715b1f143782c64e18dc33bb77487 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 14:44:35 +0800 Subject: [PATCH 093/158] Remove windows build badge [skip CI] --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 05511a7..13dad89 100644 --- a/README.rst +++ b/README.rst @@ -52,8 +52,6 @@ DEVELOPMENT .. image:: https://github.com/gitpython-developers/gitdb/workflows/Python%20package/badge.svg :target: https://github.com/gitpython-developers/gitdb/actions -.. image:: https://ci.appveyor.com/api/projects/status/2qa4km4ln7bfv76r/branch/master?svg=true&passingText=windows%20OK&failingText=windows%20failed - :target: https://ci.appveyor.com/project/ankostis/gitpython/branch/master) .. image:: https://coveralls.io/repos/gitpython-developers/gitdb/badge.png :target: https://coveralls.io/r/gitpython-developers/gitdb From 919695d4e4101237a8d2c4fb65807a42b7ff63d4 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 14:45:44 +0800 Subject: [PATCH 094/158] Also remove coveralls [skip CI] --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 13dad89..44b3edd 100644 --- a/README.rst +++ b/README.rst @@ -52,8 +52,6 @@ DEVELOPMENT .. image:: https://github.com/gitpython-developers/gitdb/workflows/Python%20package/badge.svg :target: https://github.com/gitpython-developers/gitdb/actions -.. image:: https://coveralls.io/repos/gitpython-developers/gitdb/badge.png - :target: https://coveralls.io/r/gitpython-developers/gitdb The library is considered mature, and not under active development. It's primary (known) use is in git-python. From a5d3d7e7ec4e2b52c93509bdf35999d66f91e06d Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 11 Apr 2020 19:41:43 +0800 Subject: [PATCH 095/158] change package signing key back to what it was --- Makefile | 2 +- gitdb/ext/smmap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 82c0a3b..a5098b7 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ release:: clean force_release:: clean git push --tags python3 setup.py sdist bdist_wheel - twine upload -s -i 763629FEC8788FC35128B5F6EE029D1E5EB40300 dist/* + twine upload -s -i 2CF6E0B51AAF73F09B1C21174D1DA68C88710E60 dist/* doc:: make -C doc/ html diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index f4d7a58..09f96f2 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit f4d7a58b4d96200cd057a38a0758d3c84901f57e +Subproject commit 09f96f289dbb674e64668bcb0a088aae9dff2a29 From 163f2649e5a5f7b8ba03fc1714bf4693b1a015d0 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Tue, 5 May 2020 11:45:15 +0800 Subject: [PATCH 096/158] Bump patch level for creating a new properly signed release --- doc/source/changes.rst | 6 ++++++ gitdb/__init__.py | 2 +- gitdb/ext/smmap | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 69a6dba..05748a0 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ######### +***** +4.0.5 +***** + +* Re-release of 4.0.4, with known signature + ***** 4.0.4 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 5f56773..813fe5a 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -30,7 +30,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 4) +version_info = (4, 0, 5) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 09f96f2..5e5f940 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 09f96f289dbb674e64668bcb0a088aae9dff2a29 +Subproject commit 5e5f940dff80beaa3eedf9342ef502f5e630d5ed diff --git a/setup.py b/setup.py index 48d641a..facdf3d 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 4) +version_info = (4, 0, 5) __version__ = '.'.join(str(i) for i in version_info) setup( From e5410b4166d177f90901db4986753787d34bc48f Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Fri, 12 Jun 2020 13:52:38 +0300 Subject: [PATCH 097/158] Fix exception causes in loose.py --- gitdb/db/loose.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 7bf92da..a63a2ef 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -138,12 +138,12 @@ def _map_loose_object(self, sha): # try again without noatime try: return file_contents_ro_filepath(db_path) - except OSError: - raise BadObject(sha) + except OSError as new_e: + raise BadObject(sha) from new_e # didn't work because of our flag, don't try it again self._fd_open_flags = 0 else: - raise BadObject(sha) + raise BadObject(sha) from e # END handle error # END exception handling From 112252cef0d418fd070671e64b18558c2f2cf2f1 Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Sun, 14 Jun 2020 14:35:48 +0300 Subject: [PATCH 098/158] Fix exception causes all over the codebase --- gitdb/__init__.py | 4 ++-- gitdb/db/mem.py | 4 ++-- gitdb/util.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 5f56773..31e4d45 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -18,8 +18,8 @@ def _init_externals(): try: __import__(module) - except ImportError: - raise ImportError("'%s' could not be imported, assure it is located in your PYTHONPATH" % module) + except ImportError as e: + raise ImportError("'%s' could not be imported, assure it is located in your PYTHONPATH" % module) from e # END verify import # END handel imports diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index 8711334..5b242e4 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -74,8 +74,8 @@ def stream(self, sha): # rewind stream for the next one to read ostream.stream.seek(0) return ostream - except KeyError: - raise BadObject(sha) + except KeyError as e: + raise BadObject(sha) from e # END exception handling def size(self): diff --git a/gitdb/util.py b/gitdb/util.py index d680f97..c4cafec 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -326,8 +326,8 @@ def open(self, write=False, stream=False): else: self._fd = fd # END handle file descriptor - except OSError: - raise IOError("Lock at %r could not be obtained" % self._lockfilepath()) + except OSError as e: + raise IOError("Lock at %r could not be obtained" % self._lockfilepath()) from e # END handle lock retrieval # open actual file if required From c96c755fa30277fbaadf79603a0b4fa1054ce2cb Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Mon, 15 Jun 2020 10:48:01 +0300 Subject: [PATCH 099/158] Add Ram Rachum to AUTHORS --- AUTHORS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AUTHORS b/AUTHORS index 490baad..6c7e9b9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,4 @@ Creator: Sebastian Thiel + +Contributors: + - Ram Rachum (@cool-RR) From 447c8a4b0cc6e8fbc4a20a5a6e2c7cfabe05368e Mon Sep 17 00:00:00 2001 From: Tom McClintock Date: Wed, 24 Mar 2021 08:57:44 -0700 Subject: [PATCH 100/158] Bumped smmap upper bound --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b6ccf50..72957a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -smmap>=3.0.1,<4 +smmap>=3.0.1,<5 From aa7228e8dbdc2ee6b6bc385e8bee21245a10e98d Mon Sep 17 00:00:00 2001 From: Harmon Date: Thu, 25 Mar 2021 06:29:35 -0500 Subject: [PATCH 101/158] v4.0.6 --- doc/source/changes.rst | 8 ++++++++ gitdb/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 05748a0..6217ffb 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,14 @@ Changelog ######### +***** +4.0.6 +***** + +* Bumped upper bound for smmap requirement + (`#67 `_, + `#68 `_) + ***** 4.0.5 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 2e127ca..ea7d1bc 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -30,7 +30,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 5) +version_info = (4, 0, 6) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index facdf3d..071b8c6 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 5) +version_info = (4, 0, 6) __version__ = '.'.join(str(i) for i in version_info) setup( From fc11a03b6d94cdf9d5841595caf104c2982934bb Mon Sep 17 00:00:00 2001 From: Harmon Date: Fri, 26 Mar 2021 07:41:26 -0500 Subject: [PATCH 102/158] Update smmap upper bound in setup.py Fixes #69 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 071b8c6..edbeeff 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ packages=('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), license="BSD License", zip_safe=False, - install_requires=['smmap>=3.0.1,<4'], + install_requires=['smmap>=3.0.1,<5'], long_description="""GitDB is a pure-Python git object database""", python_requires='>=3.4', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers From 6b997cd5fd01dd91ecb08d39e5e9736bc1dc9ba5 Mon Sep 17 00:00:00 2001 From: Harmon Date: Fri, 26 Mar 2021 07:43:45 -0500 Subject: [PATCH 103/158] v4.0.7 --- doc/source/changes.rst | 7 +++++++ gitdb/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 6217ffb..1a41d53 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,13 @@ Changelog ######### +***** +4.0.7 +***** + +* Updated upper bound for smmap requirement in setup.py + (`#69 `_) + ***** 4.0.6 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index ea7d1bc..bef9696 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -30,7 +30,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 6) +version_info = (4, 0, 7) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index edbeeff..59817d8 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 6) +version_info = (4, 0, 7) __version__ = '.'.join(str(i) for i in version_info) setup( From 03ab3a1d40c04d6a944299c21db61cf9ce30f6bb Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 25 Mar 2021 20:46:35 +0800 Subject: [PATCH 104/158] change signing key to the one I have --- Makefile | 2 +- gitdb/ext/smmap | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a5098b7..8290756 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ release:: clean force_release:: clean git push --tags python3 setup.py sdist bdist_wheel - twine upload -s -i 2CF6E0B51AAF73F09B1C21174D1DA68C88710E60 dist/* + twine upload -s -i 27C50E7F590947D7273A741E85194C08421980C9 dist/* doc:: make -C doc/ html diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 5e5f940..30e93fe 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 5e5f940dff80beaa3eedf9342ef502f5e630d5ed +Subproject commit 30e93fee57286afae25c28a97ba65a9770f9a729 From dff15cd8ba473776f76e8a3b6359a861e72d74aa Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 19 Aug 2021 12:09:11 +0300 Subject: [PATCH 105/158] Replace deprecated unittest aliases --- gitdb/test/db/lib.py | 4 ++-- gitdb/test/db/test_git.py | 2 +- gitdb/test/db/test_loose.py | 2 +- gitdb/test/db/test_pack.py | 2 +- gitdb/test/test_pack.py | 2 +- gitdb/test/test_stream.py | 2 +- gitdb/test/test_util.py | 6 +++--- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/gitdb/test/db/lib.py b/gitdb/test/db/lib.py index c6f4316..3df326b 100644 --- a/gitdb/test/db/lib.py +++ b/gitdb/test/db/lib.py @@ -102,8 +102,8 @@ def _assert_object_writing(self, db): assert ostream.type == str_blob_type assert ostream.size == len(data) else: - self.failUnlessRaises(BadObject, db.info, sha) - self.failUnlessRaises(BadObject, db.stream, sha) + self.assertRaises(BadObject, db.info, sha) + self.assertRaises(BadObject, db.stream, sha) # DIRECT STREAM COPY # our data hase been written in object format to the StringIO diff --git a/gitdb/test/db/test_git.py b/gitdb/test/db/test_git.py index acc0f15..6ecf7d7 100644 --- a/gitdb/test/db/test_git.py +++ b/gitdb/test/db/test_git.py @@ -43,7 +43,7 @@ def test_reading(self): assert gdb.partial_to_complete_sha_hex(bin_to_hex(binsha)[:8 - (i % 2)]) == binsha # END for each sha - self.failUnlessRaises(BadObject, gdb.partial_to_complete_sha_hex, "0000") + self.assertRaises(BadObject, gdb.partial_to_complete_sha_hex, "0000") @with_rw_directory def test_writing(self, path): diff --git a/gitdb/test/db/test_loose.py b/gitdb/test/db/test_loose.py index 9c25a02..8cc660b 100644 --- a/gitdb/test/db/test_loose.py +++ b/gitdb/test/db/test_loose.py @@ -32,5 +32,5 @@ def test_basics(self, path): assert bin_to_hex(ldb.partial_to_complete_sha_hex(short_sha)) == long_sha # END for each sha - self.failUnlessRaises(BadObject, ldb.partial_to_complete_sha_hex, '0000') + self.assertRaises(BadObject, ldb.partial_to_complete_sha_hex, '0000') # raises if no object could be found diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index 9694238..458d804 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -84,4 +84,4 @@ def test_writing(self, path): # assert num_ambiguous # non-existing - self.failUnlessRaises(BadObject, pdb.partial_to_complete_sha, b'\0\0', 4) + self.assertRaises(BadObject, pdb.partial_to_complete_sha, b'\0\0', 4) diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 8bf78f0..48a1852 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -73,7 +73,7 @@ def _assert_index_file(self, index, version, size): assert index.partial_sha_to_index(sha[:l], l * 2) == oidx # END for each object index in indexfile - self.failUnlessRaises(ValueError, index.partial_sha_to_index, "\0", 2) + self.assertRaises(ValueError, index.partial_sha_to_index, "\0", 2) def _assert_pack_file(self, pack, version, size): assert pack.version() == 2 diff --git a/gitdb/test/test_stream.py b/gitdb/test/test_stream.py index 9626825..5d4b93d 100644 --- a/gitdb/test/test_stream.py +++ b/gitdb/test/test_stream.py @@ -135,7 +135,7 @@ def test_compressed_writer(self): ostream.close() # its closed already - self.failUnlessRaises(OSError, os.close, fd) + self.assertRaises(OSError, os.close, fd) # read everything back, compare to data we zip fd = os.open(path, os.O_RDONLY | getattr(os, 'O_BINARY', 0)) diff --git a/gitdb/test/test_util.py b/gitdb/test/test_util.py index 847bdab..3b3165d 100644 --- a/gitdb/test/test_util.py +++ b/gitdb/test/test_util.py @@ -40,8 +40,8 @@ def test_lockedfd(self): lockfilepath = lfd._lockfilepath() # cannot end before it was started - self.failUnlessRaises(AssertionError, lfd.rollback) - self.failUnlessRaises(AssertionError, lfd.commit) + self.assertRaises(AssertionError, lfd.rollback) + self.assertRaises(AssertionError, lfd.commit) # open for writing assert not os.path.isfile(lockfilepath) @@ -77,7 +77,7 @@ def test_lockedfd(self): wfdstream = lfd.open(write=True, stream=True) # this time as stream assert os.path.isfile(lockfilepath) # another one fails - self.failUnlessRaises(IOError, olfd.open) + self.assertRaises(IOError, olfd.open) wfdstream.write(new_data.encode("ascii")) lfd.commit() From 1003c6612e0ee8973ba701e317b7308b7d0136aa Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 19 Aug 2021 12:22:22 +0300 Subject: [PATCH 106/158] Fix Sphinx warnings --- doc/source/conf.py | 2 +- gitdb/db/base.py | 5 +++++ gitdb/db/mem.py | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 68d9a3f..3ab15ab 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -120,7 +120,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['.static'] +#html_static_path = ['.static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/gitdb/db/base.py b/gitdb/db/base.py index 2d7b9fa..f0b8a05 100644 --- a/gitdb/db/base.py +++ b/gitdb/db/base.py @@ -33,6 +33,9 @@ def __contains__(self, sha): #{ Query Interface def has_object(self, sha): """ + Whether the object identified by the given 20 bytes + binary sha is contained in the database + :return: True if the object identified by the given 20 bytes binary sha is contained in the database""" raise NotImplementedError("To be implemented in subclass") @@ -82,6 +85,8 @@ def set_ostream(self, stream): def ostream(self): """ + Return the output stream + :return: overridden output stream this instance will write to, or None if it will write to the default stream""" return self._ostream diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index 5b242e4..212a68f 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -92,6 +92,7 @@ def stream_copy(self, sha_iter, odb): """Copy the streams as identified by sha's yielded by sha_iter into the given odb The streams will be copied directly **Note:** the object will only be written if it did not exist in the target db + :return: amount of streams actually copied into odb. If smaller than the amount of input shas, one or more objects did already exist in odb""" count = 0 From 01b6510de861ed19958ecdd445afaccd2d8a7951 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 19 Aug 2021 12:31:04 +0300 Subject: [PATCH 107/158] Remove redundant Python 2.6 code --- gitdb/stream.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gitdb/stream.py b/gitdb/stream.py index b94ef24..d58d1a6 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -30,7 +30,6 @@ from gitdb.utils.encoding import force_bytes has_perf_mod = False -PY26 = sys.version_info[:2] < (2, 7) try: from gitdb_speedups._perf import apply_delta as c_apply_delta has_perf_mod = True @@ -295,7 +294,7 @@ def read(self, size=-1): # However, the zlib VERSIONs as well as the platform check is used to further match the entries in the # table in the github issue. This is it ... it was the only way I could make this work everywhere. # IT's CERTAINLY GOING TO BITE US IN THE FUTURE ... . - if PY26 or ((zlib.ZLIB_VERSION == '1.2.7' or zlib.ZLIB_VERSION == '1.2.5') and not sys.platform == 'darwin'): + if zlib.ZLIB_VERSION in ('1.2.7', '1.2.5') and not sys.platform == 'darwin': unused_datalen = len(self._zip.unconsumed_tail) else: unused_datalen = len(self._zip.unconsumed_tail) + len(self._zip.unused_data) From e4cd296a2101df10e400ec2f1f7ac8b5ac2c37eb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 19 Aug 2021 12:32:36 +0300 Subject: [PATCH 108/158] Remove redundant Python 2 code --- gitdb/db/mem.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index 212a68f..b2542ff 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -82,10 +82,7 @@ def size(self): return len(self._cache) def sha_iter(self): - try: - return self._cache.iterkeys() - except AttributeError: - return self._cache.keys() + return self._cache.keys() #{ Interface def stream_copy(self, sha_iter, odb): From 0df4b09bac21b7d3e50931c01ccf261419f5e1ef Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 19 Aug 2021 12:42:16 +0300 Subject: [PATCH 109/158] Support Python 3.7-3.9 --- .github/workflows/pythonpackage.yml | 4 ++-- setup.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 4c67fbf..7f35b61 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,14 +15,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 with: fetch-depth: 1000 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/setup.py b/setup.py index 59817d8..522a88f 100755 --- a/setup.py +++ b/setup.py @@ -38,6 +38,8 @@ "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7" + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", ] ) From 47a74ca590efbdc3ae59277a491562efa7acc605 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 23 Oct 2021 08:29:50 +0800 Subject: [PATCH 110/158] Allow usage of smmap 5.0 (#76) --- gitdb/ext/smmap | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 30e93fe..db88100 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 30e93fee57286afae25c28a97ba65a9770f9a729 +Subproject commit db8810096503dd8a1f5a021ff39be907417f90a7 diff --git a/requirements.txt b/requirements.txt index 72957a2..1b2e11d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -smmap>=3.0.1,<5 +smmap>=3.0.1,<6 From 9a289258074fbf92a64186274067a46f7b27666e Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sat, 23 Oct 2021 08:36:34 +0800 Subject: [PATCH 111/158] Drop support for python 3.4/3.5; make use of smmap 5.0 which does the same --- doc/source/changes.rst | 8 ++++++++ gitdb/__init__.py | 2 +- setup.py | 8 +++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 1a41d53..de448dd 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,14 @@ Changelog ######### +***** +4.0.8 +***** + +* drop support for python 3.4 and 3.5 due to EOL +* Updated upper bound for smmap requirement in setup.py + (`#69 `_) + ***** 4.0.7 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index bef9696..0d936db 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -30,7 +30,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 7) +version_info = (4, 0, 8) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index 522a88f..251ea93 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 7) +version_info = (4, 0, 8) __version__ = '.'.join(str(i) for i in version_info) setup( @@ -20,9 +20,9 @@ packages=('gitdb', 'gitdb.db', 'gitdb.utils', 'gitdb.test'), license="BSD License", zip_safe=False, - install_requires=['smmap>=3.0.1,<5'], + install_requires=['smmap>=3.0.1,<6'], long_description="""GitDB is a pure-Python git object database""", - python_requires='>=3.4', + python_requires='>=3.6', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ "Development Status :: 5 - Production/Stable", @@ -35,8 +35,6 @@ "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 2913a6454c9dfc803679dc5f75315e2d821ee977 Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Wed, 5 Jan 2022 21:58:22 +0800 Subject: [PATCH 112/158] Fix typos --- gitdb/__init__.py | 2 +- gitdb/db/pack.py | 2 +- gitdb/fun.py | 8 ++++---- gitdb/pack.py | 4 ++-- gitdb/stream.py | 2 +- gitdb/test/db/lib.py | 2 +- gitdb/test/db/test_pack.py | 2 +- gitdb/test/test_pack.py | 2 +- gitdb/util.py | 6 +++--- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 0d936db..b5d1939 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -21,7 +21,7 @@ def _init_externals(): except ImportError as e: raise ImportError("'%s' could not be imported, assure it is located in your PYTHONPATH" % module) from e # END verify import - # END handel imports + # END handle imports #} END initialization diff --git a/gitdb/db/pack.py b/gitdb/db/pack.py index 177ed7b..90de02b 100644 --- a/gitdb/db/pack.py +++ b/gitdb/db/pack.py @@ -131,7 +131,7 @@ def store(self, istream): def update_cache(self, force=False): """ - Update our cache with the acutally existing packs on disk. Add new ones, + Update our cache with the actually existing packs on disk. Add new ones, and remove deleted ones. We keep the unchanged ones :param force: If True, the cache will be updated even though the directory diff --git a/gitdb/fun.py b/gitdb/fun.py index 9846597..abb4277 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -103,7 +103,7 @@ def delta_chunk_apply(dc, bbuf, write): write(bbuf[dc.so:dc.so + dc.ts]) else: # APPEND DATA - # whats faster: if + 4 function calls or just a write with a slice ? + # what's faster: if + 4 function calls or just a write with a slice ? # Considering data can be larger than 127 bytes now, it should be worth it if dc.ts < len(dc.data): write(dc.data[:dc.ts]) @@ -292,7 +292,7 @@ def check_integrity(self, target_size=-1): """Verify the list has non-overlapping chunks only, and the total size matches target_size :param target_size: if not -1, the total size of the chain must be target_size - :raise AssertionError: if the size doen't match""" + :raise AssertionError: if the size doesn't match""" if target_size > -1: assert self[-1].rbound() == target_size assert reduce(lambda x, y: x + y, (d.ts for d in self), 0) == target_size @@ -331,7 +331,7 @@ def connect_with_next_base(self, bdcl): cannot be changed by any of the upcoming bases anymore. Once all our chunks are marked like that, we can stop all processing :param bdcl: data chunk list being one of our bases. They must be fed in - consequtively and in order, towards the earliest ancestor delta + consecutively and in order, towards the earliest ancestor delta :return: True if processing was done. Use it to abort processing of remaining streams if False is returned""" nfc = 0 # number of frozen chunks @@ -624,7 +624,7 @@ def apply_delta_data(src_buf, src_buf_size, delta_buf, delta_buf_size, write): :param src_buf: random access data from which the delta was created :param src_buf_size: size of the source buffer in bytes - :param delta_buf_size: size fo the delta buffer in bytes + :param delta_buf_size: size for the delta buffer in bytes :param delta_buf: random access delta data :param write: write method taking a chunk of bytes diff --git a/gitdb/pack.py b/gitdb/pack.py index a38468e..0b26c12 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -224,7 +224,7 @@ def write(self, pack_sha, write): if ofs > 0x7fffffff: tmplist.append(ofs) ofs = 0x80000000 + len(tmplist) - 1 - # END hande 64 bit offsets + # END handle 64 bit offsets sha_write(pack('>L', ofs & 0xffffffff)) # END for each offset @@ -506,7 +506,7 @@ class PackFile(LazyMixin): """A pack is a file written according to the Version 2 for git packs As we currently use memory maps, it could be assumed that the maximum size of - packs therefor is 32 bit on 32 bit systems. On 64 bit systems, this should be + packs therefore is 32 bit on 32 bit systems. On 64 bit systems, this should be fine though. **Note:** at some point, this might be implemented using streams as well, or diff --git a/gitdb/stream.py b/gitdb/stream.py index d58d1a6..37380ad 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -289,7 +289,7 @@ def read(self, size=-1): # They are thorough, and I assume it is truly working. # Why is this logic as convoluted as it is ? Please look at the table in # https://github.com/gitpython-developers/gitdb/issues/19 to learn about the test-results. - # Bascially, on py2.6, you want to use branch 1, whereas on all other python version, the second branch + # Basically, on py2.6, you want to use branch 1, whereas on all other python version, the second branch # will be the one that works. # However, the zlib VERSIONs as well as the platform check is used to further match the entries in the # table in the github issue. This is it ... it was the only way I could make this work everywhere. diff --git a/gitdb/test/db/lib.py b/gitdb/test/db/lib.py index 3df326b..b38f1d5 100644 --- a/gitdb/test/db/lib.py +++ b/gitdb/test/db/lib.py @@ -107,7 +107,7 @@ def _assert_object_writing(self, db): # DIRECT STREAM COPY # our data hase been written in object format to the StringIO - # we pasesd as output stream. No physical database representation + # we passed as output stream. No physical database representation # was created. # Test direct stream copy of object streams, the result must be # identical to what we fed in diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index 458d804..ff96a58 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -80,7 +80,7 @@ def test_writing(self, path): # END for each sha to find # we should have at least one ambiguous, considering the small sizes - # but in our pack, there is no ambigious ... + # but in our pack, there is no ambiguous ... # assert num_ambiguous # non-existing diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 48a1852..4b01741 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -101,7 +101,7 @@ def _assert_pack_file(self, pack, version, size): dstream = DeltaApplyReader.new(streams) except ValueError: # ignore these, old git versions use only ref deltas, - # which we havent resolved ( as we are without an index ) + # which we haven't resolved ( as we are without an index ) # Also ignore non-delta streams continue # END get deltastream diff --git a/gitdb/util.py b/gitdb/util.py index c4cafec..f9f8c0e 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -182,7 +182,7 @@ def file_contents_ro(fd, stream=False, allow_mmap=True): pass # END exception handling - # read manully + # read manually contents = os.read(fd, os.fstat(fd).st_size) if stream: return _RandomAccessBytesIO(contents) @@ -248,7 +248,7 @@ class LazyMixin(object): def __getattr__(self, attr): """ Whenever an attribute is requested that we do not know, we allow it - to be created and set. Next time the same attribute is reqeusted, it is simply + to be created and set. Next time the same attribute is requested, it is simply returned from our dict/slots. """ self._set_cache_(attr) # will raise in case the cache was not created @@ -332,7 +332,7 @@ def open(self, write=False, stream=False): # open actual file if required if self._fd is None: - # we could specify exlusive here, as we obtained the lock anyway + # we could specify exclusive here, as we obtained the lock anyway try: self._fd = os.open(self._filepath, os.O_RDONLY | binary) except: From e3a4cfe0ef2bc7b9c785c4fec650f5045cdd1e50 Mon Sep 17 00:00:00 2001 From: Carl George Date: Wed, 9 Feb 2022 17:15:39 -0600 Subject: [PATCH 113/158] Switch from nose to pytest This is not a full rewrite to pytest style tests, it just changes the minimum to allow pytest to run the existing tests. Resolves #72 --- .github/workflows/pythonpackage.yml | 6 +++--- Makefile | 3 +-- README.rst | 4 ++-- gitdb.pro.user | 3 +-- gitdb/test/db/test_pack.py | 4 ++-- gitdb/test/lib.py | 4 ++-- gitdb/test/test_pack.py | 4 ++-- 7 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 7f35b61..c695ad3 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -36,9 +36,9 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with nose + - name: Test with pytest run: | - pip install nose + pip install pytest ulimit -n 48 ulimit -n - nosetests -v + pytest -v diff --git a/Makefile b/Makefile index 8290756..e063028 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ PYTHON = python SETUP = $(PYTHON) setup.py -TESTRUNNER = $(shell which nosetests) TESTFLAGS = all:: @@ -37,5 +36,5 @@ clean:: rm -f *.so coverage:: build - PYTHONPATH=. $(PYTHON) $(TESTRUNNER) --cover-package=gitdb --with-coverage --cover-erase --cover-inclusive gitdb + PYTHONPATH=. $(PYTHON) -m pytest --cov=gitdb gitdb diff --git a/README.rst b/README.rst index 44b3edd..29c70f7 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ If you want to go up to 20% faster, you can install gitdb-speedups with: REQUIREMENTS ============ -* Python Nose - for running the tests +* pytest - for running the tests SOURCE ====== @@ -45,7 +45,7 @@ Once the clone is complete, please be sure to initialize the submodules using Run the tests with - nosetests + pytest DEVELOPMENT =========== diff --git a/gitdb.pro.user b/gitdb.pro.user index 398cb70..3ca1e21 100644 --- a/gitdb.pro.user +++ b/gitdb.pro.user @@ -233,8 +233,7 @@ - /usr/bin/nosetests - -s + /usr/bin/pytest gitdb/test/test_pack.py 2 diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index ff96a58..4539f42 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -16,7 +16,7 @@ import random import sys -from nose.plugins.skip import SkipTest +import pytest class TestPackDB(TestDBBase): @@ -24,7 +24,7 @@ class TestPackDB(TestDBBase): @with_packs_rw def test_writing(self, path): if sys.platform == "win32": - raise SkipTest("FIXME: Currently fail on windows") + pytest.skip("FIXME: Currently fail on windows") pdb = PackedDB(path) diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index a04084f..abd4ad5 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -65,8 +65,8 @@ def skip_on_travis_ci(func): @wraps(func) def wrapper(self, *args, **kwargs): if 'TRAVIS' in os.environ: - import nose - raise nose.SkipTest("Cannot run on travis-ci") + import pytest + pytest.skip("Cannot run on travis-ci") # end check for travis ci return func(self, *args, **kwargs) # end wrapper diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index 4b01741..f946197 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -26,7 +26,7 @@ from gitdb.exc import UnsupportedOperation from gitdb.util import to_bin_sha -from nose import SkipTest +import pytest import os import tempfile @@ -246,4 +246,4 @@ def rewind_streams(): def test_pack_64(self): # TODO: hex-edit a pack helping us to verify that we can handle 64 byte offsets # of course without really needing such a huge pack - raise SkipTest() + pytest.skip('not implemented') From 7c67010acf541b4269825cb2c13e226fe2d65ea9 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 24 Oct 2021 21:38:51 +0800 Subject: [PATCH 114/158] bump patch level in the hopes for a valid sig on release https://github.com/gitpython-developers/gitdb/issues/77 --- doc/source/changes.rst | 6 ++++++ gitdb/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index de448dd..5ef1326 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ######### +***** +4.0.9 +***** + +- re-release of 4.0.8 to get a valid signature. + ***** 4.0.8 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index b5d1939..2460145 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -30,7 +30,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 8) +version_info = (4, 0, 9) __version__ = '.'.join(str(i) for i in version_info) diff --git a/setup.py b/setup.py index 251ea93..31dc62f 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 8) +version_info = (4, 0, 9) __version__ = '.'.join(str(i) for i in version_info) setup( From 4762d99d978586fcdf08ade552f4712bfde6ef22 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Mon, 21 Feb 2022 10:43:54 +0800 Subject: [PATCH 115/158] Stop signing releases (#77) The key stopped producing correct signatures when upgrading MacOS, at least when used by `twine`. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e063028..a7acbb1 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ release:: clean force_release:: clean git push --tags python3 setup.py sdist bdist_wheel - twine upload -s -i 27C50E7F590947D7273A741E85194C08421980C9 dist/* + twine upload dist/* doc:: make -C doc/ html From 2ce0e3175bcbbf397d25f18b1008d1fdf54611f2 Mon Sep 17 00:00:00 2001 From: randymcmillan Date: Tue, 15 Mar 2022 17:51:07 -0400 Subject: [PATCH 116/158] .gitmodules: update smmap url Github.com is currenly redirecting the smmap url: https://github.com/Byron/smmap.git to: https://github.com/gitpython-developers/smmap For correctness we update to the url directly --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index d85b15c..e73cded 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "smmap"] path = gitdb/ext/smmap - url = https://github.com/Byron/smmap.git + url = https://github.com/gitpython-developers/smmap.git From 5f149c1a9c36c985158c0757377af958362b3582 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 16 Nov 2022 18:13:01 +0200 Subject: [PATCH 117/158] Add support for Python 3.10 and 3.11 --- .github/workflows/pythonpackage.yml | 6 +++--- setup.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c695ad3..3c05764 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,14 +15,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 1000 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/setup.py b/setup.py index 31dc62f..5d324cd 100755 --- a/setup.py +++ b/setup.py @@ -39,5 +39,8 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", ] ) From a6f0856d807efc1e7bc37d13f9cfbcdb91dea2ac Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 16 Nov 2022 18:15:02 +0200 Subject: [PATCH 118/158] Remove redundant Travis CI config/code --- .travis.yml | 20 ------------------- gitdb/test/lib.py | 15 -------------- gitdb/test/performance/test_pack.py | 4 ---- gitdb/test/performance/test_pack_streaming.py | 3 --- gitdb/test/performance/test_stream.py | 2 -- 5 files changed, 44 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b980d36..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -# NOT USED, just for reference. See github actions for CI configuration -language: python -python: - - "3.4" - - "3.5" - - "3.6" - # - "pypy" - won't work as smmap doesn't work (see smmap/.travis.yml for details) - -git: - # a higher depth is needed for one of the tests - lets fet - depth: 1000 -install: - - pip install coveralls -script: - - ulimit -n 48 - - ulimit -n - - nosetests -v --with-coverage -after_success: - - coveralls - diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index abd4ad5..465a899 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -58,21 +58,6 @@ def setUpClass(cls): #{ Decorators -def skip_on_travis_ci(func): - """All tests decorated with this one will raise SkipTest when run on travis ci. - Use it to workaround difficult to solve issues - NOTE: copied from bcore (https://github.com/Byron/bcore)""" - @wraps(func) - def wrapper(self, *args, **kwargs): - if 'TRAVIS' in os.environ: - import pytest - pytest.skip("Cannot run on travis-ci") - # end check for travis ci - return func(self, *args, **kwargs) - # end wrapper - return wrapper - - def with_rw_directory(func): """Create a temporary directory which can be written to, remove it if the test succeeds, but leave it otherwise to aid additional debugging""" diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index b59d5a9..643186b 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -17,7 +17,6 @@ from gitdb.typ import str_blob_type from gitdb.exc import UnsupportedOperation from gitdb.db.pack import PackedDB -from gitdb.test.lib import skip_on_travis_ci import sys import os @@ -26,7 +25,6 @@ class TestPackedDBPerformance(TestBigRepoR): - @skip_on_travis_ci def test_pack_random_access(self): pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) @@ -79,7 +77,6 @@ def test_pack_random_access(self): print("PDB: Obtained %i streams by sha and read all bytes totallying %i KiB ( %f KiB / s ) in %f s ( %f streams/s )" % (max_items, total_kib, total_kib / (elapsed or 1), elapsed, max_items / (elapsed or 1)), file=sys.stderr) - @skip_on_travis_ci def test_loose_correctness(self): """based on the pack(s) of our packed object DB, we will just copy and verify all objects in the back into the loose object db (memory). @@ -106,7 +103,6 @@ def test_loose_correctness(self): mdb._cache.clear() # end for each sha to copy - @skip_on_travis_ci def test_correctness(self): pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) # disabled for now as it used to work perfectly, checking big repositories takes a long time diff --git a/gitdb/test/performance/test_pack_streaming.py b/gitdb/test/performance/test_pack_streaming.py index 76f0f4a..5bf6790 100644 --- a/gitdb/test/performance/test_pack_streaming.py +++ b/gitdb/test/performance/test_pack_streaming.py @@ -12,7 +12,6 @@ from gitdb.db.pack import PackedDB from gitdb.stream import NullStream from gitdb.pack import PackEntity -from gitdb.test.lib import skip_on_travis_ci import os import sys @@ -34,7 +33,6 @@ def write(self, d): class TestPackStreamingPerformance(TestBigRepoR): - @skip_on_travis_ci def test_pack_writing(self): # see how fast we can write a pack from object streams. # This will not be fast, as we take time for decompressing the streams as well @@ -61,7 +59,6 @@ def test_pack_writing(self): print(sys.stderr, "PDB Streaming: Wrote pack of size %i kb in %f s (%f kb/s)" % (total_kb, elapsed, total_kb / (elapsed or 1)), sys.stderr) - @skip_on_travis_ci def test_stream_reading(self): # raise SkipTest() pdb = PackedDB(os.path.join(self.gitrepopath, "objects/pack")) diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index 92d28e4..9a8b15b 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -20,7 +20,6 @@ from gitdb.test.lib import ( make_memory_file, with_rw_directory, - skip_on_travis_ci ) @@ -44,7 +43,6 @@ class TestObjDBPerformance(TestBigRepoR): large_data_size_bytes = 1000 * 1000 * 50 # some MiB should do it moderate_data_size_bytes = 1000 * 1000 * 1 # just 1 MiB - @skip_on_travis_ci @with_rw_directory def test_large_data_streaming(self, path): ldb = LooseObjectDB(path) From faed217d14965932b333c07219ed401a661d2651 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 16 Nov 2022 18:16:09 +0200 Subject: [PATCH 119/158] Drop support for EOL Python 3.5 and 3.6 --- .github/workflows/pythonpackage.yml | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3c05764..0d039ad 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 diff --git a/setup.py b/setup.py index 5d324cd..d38b267 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ zip_safe=False, install_requires=['smmap>=3.0.1,<6'], long_description="""GitDB is a pure-Python git object database""", - python_requires='>=3.6', + python_requires='>=3.7', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ "Development Status :: 5 - Production/Stable", @@ -35,7 +35,6 @@ "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From 7a68270d6c78b81f577b433dc6351b26bc27b7cf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 16 Nov 2022 18:17:20 +0200 Subject: [PATCH 120/158] Upgrade Python syntax with pyupgrade --py37-plus --- doc/source/conf.py | 9 ++++----- gitdb/db/base.py | 12 ++++++------ gitdb/db/git.py | 4 ++-- gitdb/db/loose.py | 4 ++-- gitdb/db/mem.py | 2 +- gitdb/db/pack.py | 2 +- gitdb/db/ref.py | 8 ++++---- gitdb/fun.py | 2 +- gitdb/pack.py | 4 ++-- gitdb/stream.py | 12 ++++++------ gitdb/test/db/test_ref.py | 2 +- gitdb/test/lib.py | 6 +++--- gitdb/test/performance/test_pack.py | 1 - gitdb/test/performance/test_pack_streaming.py | 1 - gitdb/test/performance/test_stream.py | 1 - gitdb/test/test_example.py | 2 +- gitdb/test/test_stream.py | 4 ++-- gitdb/util.py | 14 +++++++------- 18 files changed, 43 insertions(+), 47 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 3ab15ab..b387f60 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # GitDB documentation build configuration file, created by # sphinx-quickstart on Wed Jun 30 00:01:32 2010. @@ -38,8 +37,8 @@ master_doc = 'index' # General information about the project. -project = u'GitDB' -copyright = u'2011, Sebastian Thiel' +project = 'GitDB' +copyright = '2011, Sebastian Thiel' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -172,8 +171,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'GitDB.tex', u'GitDB Documentation', - u'Sebastian Thiel', 'manual'), + ('index', 'GitDB.tex', 'GitDB Documentation', + 'Sebastian Thiel', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/gitdb/db/base.py b/gitdb/db/base.py index f0b8a05..e89052e 100644 --- a/gitdb/db/base.py +++ b/gitdb/db/base.py @@ -22,7 +22,7 @@ __all__ = ('ObjectDBR', 'ObjectDBW', 'FileDBBase', 'CompoundDB', 'CachingDB') -class ObjectDBR(object): +class ObjectDBR: """Defines an interface for object database lookup. Objects are identified either by their 20 byte bin sha""" @@ -63,7 +63,7 @@ def sha_iter(self): #} END query interface -class ObjectDBW(object): +class ObjectDBW: """Defines an interface to create objects in the database""" @@ -105,7 +105,7 @@ def store(self, istream): #} END edit interface -class FileDBBase(object): +class FileDBBase: """Provides basic facilities to retrieve files of interest, including caching facilities to help mapping hexsha's to objects""" @@ -117,7 +117,7 @@ def __init__(self, root_path): **Note:** The base will not perform any accessablity checking as the base might not yet be accessible, but become accessible before the first access.""" - super(FileDBBase, self).__init__() + super().__init__() self._root_path = root_path #{ Interface @@ -133,7 +133,7 @@ def db_path(self, rela_path): #} END interface -class CachingDB(object): +class CachingDB: """A database which uses caches to speed-up access""" @@ -176,7 +176,7 @@ def _set_cache_(self, attr): elif attr == '_db_cache': self._db_cache = dict() else: - super(CompoundDB, self)._set_cache_(attr) + super()._set_cache_(attr) def _db_query(self, sha): """:return: database containing the given 20 byte sha diff --git a/gitdb/db/git.py b/gitdb/db/git.py index 7a43d72..e2cb468 100644 --- a/gitdb/db/git.py +++ b/gitdb/db/git.py @@ -39,7 +39,7 @@ class GitDB(FileDBBase, ObjectDBW, CompoundDB): def __init__(self, root_path): """Initialize ourselves on a git objects directory""" - super(GitDB, self).__init__(root_path) + super().__init__(root_path) def _set_cache_(self, attr): if attr == '_dbs' or attr == '_loose_db': @@ -68,7 +68,7 @@ def _set_cache_(self, attr): # finally set the value self._loose_db = loose_db else: - super(GitDB, self)._set_cache_(attr) + super()._set_cache_(attr) # END handle attrs #{ ObjectDBW interface diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index a63a2ef..4ef7750 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -75,7 +75,7 @@ class LooseObjectDB(FileDBBase, ObjectDBR, ObjectDBW): new_objects_mode = int("644", 8) def __init__(self, root_path): - super(LooseObjectDB, self).__init__(root_path) + super().__init__(root_path) self._hexsha_to_file = dict() # Additional Flags - might be set to 0 after the first failure # Depending on the root, this might work for some mounts, for others not, which @@ -151,7 +151,7 @@ def set_ostream(self, stream): """:raise TypeError: if the stream does not support the Sha1Writer interface""" if stream is not None and not isinstance(stream, Sha1Writer): raise TypeError("Output stream musst support the %s interface" % Sha1Writer.__name__) - return super(LooseObjectDB, self).set_ostream(stream) + return super().set_ostream(stream) def info(self, sha): m = self._map_loose_object(sha) diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index b2542ff..1b954cb 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -37,7 +37,7 @@ class MemoryDB(ObjectDBR, ObjectDBW): exists in the target storage before introducing actual IO""" def __init__(self): - super(MemoryDB, self).__init__() + super().__init__() self._db = LooseObjectDB("path/doesnt/matter") # maps 20 byte shas to their OStream objects diff --git a/gitdb/db/pack.py b/gitdb/db/pack.py index 90de02b..1ce786b 100644 --- a/gitdb/db/pack.py +++ b/gitdb/db/pack.py @@ -39,7 +39,7 @@ class PackedDB(FileDBBase, ObjectDBR, CachingDB, LazyMixin): _sort_interval = 500 def __init__(self, root_path): - super(PackedDB, self).__init__(root_path) + super().__init__(root_path) # list of lists with three items: # * hits - number of times the pack was hit with a request # * entity - Pack entity instance diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index 2bb1de7..6bb2a64 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -20,7 +20,7 @@ class ReferenceDB(CompoundDB): ObjectDBCls = None def __init__(self, ref_file): - super(ReferenceDB, self).__init__() + super().__init__() self._ref_file = ref_file def _set_cache_(self, attr): @@ -28,7 +28,7 @@ def _set_cache_(self, attr): self._dbs = list() self._update_dbs_from_ref_file() else: - super(ReferenceDB, self)._set_cache_(attr) + super()._set_cache_(attr) # END handle attrs def _update_dbs_from_ref_file(self): @@ -44,7 +44,7 @@ def _update_dbs_from_ref_file(self): try: with codecs.open(self._ref_file, 'r', encoding="utf-8") as f: ref_paths = [l.strip() for l in f] - except (OSError, IOError): + except OSError: pass # END handle alternates @@ -79,4 +79,4 @@ def _update_dbs_from_ref_file(self): def update_cache(self, force=False): # re-read alternates and update databases self._update_dbs_from_ref_file() - return super(ReferenceDB, self).update_cache(force) + return super().update_cache(force) diff --git a/gitdb/fun.py b/gitdb/fun.py index abb4277..a4454de 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -113,7 +113,7 @@ def delta_chunk_apply(dc, bbuf, write): # END handle chunk mode -class DeltaChunk(object): +class DeltaChunk: """Represents a piece of a delta, it can either add new data, or copy existing one from a source buffer""" diff --git a/gitdb/pack.py b/gitdb/pack.py index 0b26c12..cabce4c 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -170,7 +170,7 @@ def write_stream_to_pack(read, write, zstream, base_crc=None): #} END utilities -class IndexWriter(object): +class IndexWriter: """Utility to cache index information, allowing to write all information later in one go to the given stream @@ -257,7 +257,7 @@ class PackIndexFile(LazyMixin): index_version_default = 2 def __init__(self, indexpath): - super(PackIndexFile, self).__init__() + super().__init__() self._indexpath = indexpath def close(self): diff --git a/gitdb/stream.py b/gitdb/stream.py index 37380ad..222b843 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -219,13 +219,13 @@ def read(self, size=-1): # END clamp size if size == 0: - return bytes() + return b'' # END handle depletion # deplete the buffer, then just continue using the decompress object # which has an own buffer. We just need this to transparently parse the # header from the zlib stream - dat = bytes() + dat = b'' if self._buf: if self._buflen >= size: # have enough data @@ -553,7 +553,7 @@ def size(self): #{ W Streams -class Sha1Writer(object): +class Sha1Writer: """Simple stream writer which produces a sha whenever you like as it degests everything it is supposed to write""" @@ -650,7 +650,7 @@ class FDCompressedSha1Writer(Sha1Writer): exc = IOError("Failed to write all bytes to filedescriptor") def __init__(self, fd): - super(FDCompressedSha1Writer, self).__init__() + super().__init__() self.fd = fd self.zip = zlib.compressobj(zlib.Z_BEST_SPEED) @@ -677,7 +677,7 @@ def close(self): #} END stream interface -class FDStream(object): +class FDStream: """A simple wrapper providing the most basic functions on a file descriptor with the fileobject interface. Cannot use os.fdopen as the resulting stream @@ -711,7 +711,7 @@ def close(self): close(self._fd) -class NullStream(object): +class NullStream: """A stream that does nothing but providing a stream interface. Use it like /dev/null""" diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index 2049698..664aa54 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -23,7 +23,7 @@ def make_alt_file(self, alt_path, alt_list): The list can be empty""" with open(alt_path, "wb") as alt_file: for alt in alt_list: - alt_file.write(alt.encode("utf-8") + "\n".encode("ascii")) + alt_file.write(alt.encode("utf-8") + b"\n") @with_rw_directory def test_writing(self, path): diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index 465a899..da59d3b 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -40,7 +40,7 @@ class TestBase(unittest.TestCase): @classmethod def setUpClass(cls): try: - super(TestBase, cls).setUpClass() + super().setUpClass() except AttributeError: pass @@ -70,7 +70,7 @@ def wrapper(self): try: return func(self, path) except Exception: - sys.stderr.write("Test {}.{} failed, output is at {!r}\n".format(type(self).__name__, func.__name__, path)) + sys.stderr.write(f"Test {type(self).__name__}.{func.__name__} failed, output is at {path!r}\n") keep = True raise finally: @@ -161,7 +161,7 @@ def make_memory_file(size_in_bytes, randomize=False): #{ Stream Utilities -class DummyStream(object): +class DummyStream: def __init__(self): self.was_read = False diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index 643186b..f034baf 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -3,7 +3,6 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Performance tests for object store""" -from __future__ import print_function from gitdb.test.performance.lib import ( TestBigRepoR diff --git a/gitdb/test/performance/test_pack_streaming.py b/gitdb/test/performance/test_pack_streaming.py index 5bf6790..db790f1 100644 --- a/gitdb/test/performance/test_pack_streaming.py +++ b/gitdb/test/performance/test_pack_streaming.py @@ -3,7 +3,6 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Specific test for pack streams only""" -from __future__ import print_function from gitdb.test.performance.lib import ( TestBigRepoR diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index 9a8b15b..91dc891 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -3,7 +3,6 @@ # This module is part of GitDB and is released under # the New BSD License: http://www.opensource.org/licenses/bsd-license.php """Performance data streaming performance""" -from __future__ import print_function from gitdb.test.performance.lib import TestBigRepoR from gitdb.db import LooseObjectDB diff --git a/gitdb/test/test_example.py b/gitdb/test/test_example.py index 6e80bf5..cc4d40d 100644 --- a/gitdb/test/test_example.py +++ b/gitdb/test/test_example.py @@ -32,7 +32,7 @@ def test_base(self): pass # END ignore exception if there are no loose objects - data = "my data".encode("ascii") + data = b"my data" istream = IStream("blob", len(data), BytesIO(data)) # the object does not yet have a sha diff --git a/gitdb/test/test_stream.py b/gitdb/test/test_stream.py index 5d4b93d..5e2e1ba 100644 --- a/gitdb/test/test_stream.py +++ b/gitdb/test/test_stream.py @@ -115,13 +115,13 @@ def test_decompress_reader(self): def test_sha_writer(self): writer = Sha1Writer() - assert 2 == writer.write("hi".encode("ascii")) + assert 2 == writer.write(b"hi") assert len(writer.sha(as_hex=1)) == 40 assert len(writer.sha(as_hex=0)) == 20 # make sure it does something ;) prev_sha = writer.sha() - writer.write("hi again".encode("ascii")) + writer.write(b"hi again") assert writer.sha() != prev_sha def test_compressed_writer(self): diff --git a/gitdb/util.py b/gitdb/util.py index f9f8c0e..3151c06 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -94,7 +94,7 @@ def remove(*args, **kwargs): #{ compatibility stuff ... -class _RandomAccessBytesIO(object): +class _RandomAccessBytesIO: """Wrapper to provide required functionality in case memory maps cannot or may not be used. This is only really required in python 2.4""" @@ -131,7 +131,7 @@ def byte_ord(b): #{ Routines -def make_sha(source=''.encode("ascii")): +def make_sha(source=b''): """A python2.4 workaround for the sha/hashlib module fiasco **Note** From the dulwich project """ @@ -151,7 +151,7 @@ def allocate_memory(size): try: return mmap.mmap(-1, size) # read-write by default - except EnvironmentError: + except OSError: # setup real memory instead # this of course may fail if the amount of memory is not available in # one chunk - would only be the case in python 2.4, being more likely on @@ -174,7 +174,7 @@ def file_contents_ro(fd, stream=False, allow_mmap=True): # supports stream and random access try: return mmap.mmap(fd, 0, access=mmap.ACCESS_READ) - except EnvironmentError: + except OSError: # python 2.4 issue, 0 wants to be the actual size return mmap.mmap(fd, os.fstat(fd).st_size, access=mmap.ACCESS_READ) # END handle python 2.4 @@ -234,7 +234,7 @@ def to_bin_sha(sha): #{ Utilities -class LazyMixin(object): +class LazyMixin: """ Base class providing an interface to lazily retrieve attribute values upon @@ -266,7 +266,7 @@ def _set_cache_(self, attr): pass -class LockedFD(object): +class LockedFD: """ This class facilitates a safe read and write operation to a file on disk. @@ -327,7 +327,7 @@ def open(self, write=False, stream=False): self._fd = fd # END handle file descriptor except OSError as e: - raise IOError("Lock at %r could not be obtained" % self._lockfilepath()) from e + raise OSError("Lock at %r could not be obtained" % self._lockfilepath()) from e # END handle lock retrieval # open actual file if required From c3ab5d7b28062848c2a639a60e0acfbaee7e8f90 Mon Sep 17 00:00:00 2001 From: zwimer Date: Tue, 22 Nov 2022 18:57:47 -0700 Subject: [PATCH 121/158] Prefer import to __import__ --- .gitignore | 1 + gitdb/__init__.py | 15 ++++++--------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index e0b4e85..8b7da92 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ *.so .noseids *.sublime-workspace +*.egg-info diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 2460145..c5b5547 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -12,15 +12,12 @@ def _init_externals(): """Initialize external projects by putting them into the path""" - for module in ('smmap',): - if 'PYOXIDIZER' not in os.environ: - sys.path.append(os.path.join(os.path.dirname(__file__), 'ext', module)) - - try: - __import__(module) - except ImportError as e: - raise ImportError("'%s' could not be imported, assure it is located in your PYTHONPATH" % module) from e - # END verify import + if 'PYOXIDIZER' not in os.environ: + where = os.path.join(os.path.dirname(__file__), 'ext', 'smmap') + if os.path.exists(where): + sys.path.append(where) + + import smmap # END handle imports #} END initialization From 1edc7d296af635dc31030a09e73fd684eedc1d59 Mon Sep 17 00:00:00 2001 From: zwimer Date: Tue, 22 Nov 2022 19:05:11 -0700 Subject: [PATCH 122/158] Add del smmap to match previous behavior --- gitdb/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index c5b5547..94b0831 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -18,6 +18,7 @@ def _init_externals(): sys.path.append(where) import smmap + del smmap # END handle imports #} END initialization From 38c68d95eaed9bf7415781ca0102441a349893ee Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 24 Nov 2022 06:22:21 +0100 Subject: [PATCH 123/158] bump version to 4.0.10 --- doc/source/changes.rst | 6 ++++++ gitdb/__init__.py | 2 +- gitdb/ext/smmap | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 5ef1326..7d85ea5 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ######### +****** +4.0.10 +****** + +- improvements to the way external packages are imported. + ***** 4.0.9 ***** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 94b0831..b777632 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -28,7 +28,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 9) +version_info = (4, 0, 10) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index db88100..334ef84 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit db8810096503dd8a1f5a021ff39be907417f90a7 +Subproject commit 334ef84a05c953ed5dbec7b9c6d4310879eeab5a diff --git a/setup.py b/setup.py index d38b267..5214849 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 9) +version_info = (4, 0, 10) __version__ = '.'.join(str(i) for i in version_info) setup( From 49c3178711ddb3510f0e96297187f823cc019871 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 24 Nov 2022 06:24:37 +0100 Subject: [PATCH 124/158] use python3 binary MacOS Ventura doesn't come with a `python` binary anymore --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a7acbb1..7aa5a71 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PYTHON = python +PYTHON = python3 SETUP = $(PYTHON) setup.py TESTFLAGS = From b6faecc46fcc4f6412149f2021447c0070eba60e Mon Sep 17 00:00:00 2001 From: Ondrej Tethal Date: Fri, 9 Dec 2022 10:21:58 +0100 Subject: [PATCH 125/158] Use ZLIB_RUNTIME_VERSION if available --- gitdb/stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitdb/stream.py b/gitdb/stream.py index 222b843..1b5426f 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -294,7 +294,7 @@ def read(self, size=-1): # However, the zlib VERSIONs as well as the platform check is used to further match the entries in the # table in the github issue. This is it ... it was the only way I could make this work everywhere. # IT's CERTAINLY GOING TO BITE US IN THE FUTURE ... . - if zlib.ZLIB_VERSION in ('1.2.7', '1.2.5') and not sys.platform == 'darwin': + if getattr(zlib, 'ZLIB_RUNTIME_VERSION', zlib.ZLIB_VERSION) in ('1.2.7', '1.2.5') and not sys.platform == 'darwin': unused_datalen = len(self._zip.unconsumed_tail) else: unused_datalen = len(self._zip.unconsumed_tail) + len(self._zip.unused_data) From 32d12aa03aff193603aad1d6f08669a70607beb9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 10 Sep 2023 18:38:38 +0300 Subject: [PATCH 126/158] Bump GitHub Actions --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 0d039ad..1cefabc 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -18,7 +18,7 @@ jobs: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1000 - name: Set up Python ${{ matrix.python-version }} From d7fc1fd3d154c809b1c021ae3fa564e9fe4b4859 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 10 Sep 2023 18:38:56 +0300 Subject: [PATCH 127/158] Add support for Python 3.12 --- .github/workflows/pythonpackage.yml | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 1cefabc..dab41d0 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 @@ -25,6 +25,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/setup.py b/setup.py index 5214849..61b5727 100755 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", ] ) From 875acb45cd8e9353c1911717bd2cd05b3e40ed05 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 10 Sep 2023 18:39:33 +0300 Subject: [PATCH 128/158] Drop support for EOL Python 3.7 --- .github/workflows/pythonpackage.yml | 2 +- setup.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index dab41d0..98e670c 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/setup.py b/setup.py index 61b5727..5734122 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ zip_safe=False, install_requires=['smmap>=3.0.1,<6'], long_description="""GitDB is a pure-Python git object database""", - python_requires='>=3.7', + python_requires='>=3.8', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ "Development Status :: 5 - Production/Stable", @@ -35,7 +35,6 @@ "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", From 8ac188497e7ac1ef2e89c984ac3728319b625e1a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 10 Sep 2023 18:14:06 -0400 Subject: [PATCH 129/158] Enable Dependabot version updates for Actions This enables Dependabot version updates for GitHub Actions only (not Python dependencies), using the exact same configuration as in GitPython. --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..203f3c8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From e7937aefaf49c72b5f4432cc1f303ed23889589a Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 02:31:34 -0400 Subject: [PATCH 130/158] Test installing project on CI This changes the CI workflow to install the project itself to get its dependency, rather than installing the dependency from requirements.txt. This is the more important thing to test, because it verifies that the project is installable and effectively declares the dependencies it needs. --- .github/workflows/pythonpackage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 98e670c..efa04a9 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -26,10 +26,10 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - name: Install dependencies + - name: Install project and dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install . - name: Lint with flake8 run: | pip install flake8 From 60f7f94607996e05292896e8105ef3780779959d Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 03:38:59 -0400 Subject: [PATCH 131/158] Fix mkdir race condition in LooseObjectDB.store This replaces the conditional call to os.mkdir that raises an unintended FileExistsError if the directory is created between the check and the os.mkdir call, using a single os.makedirs call instead, with exist_ok=True. This way, we attempt creation in a way that produces no error if the directory is already present, while still raising FileExistsError if a non-directory filesystem entry (such as a regular file) is present where we want the directory to be. --- gitdb/db/loose.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 4ef7750..7ea6fef 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -8,7 +8,6 @@ ObjectDBW ) - from gitdb.exc import ( BadObject, AmbiguousObjectName @@ -33,10 +32,8 @@ bin_to_hex, exists, chmod, - isdir, isfile, remove, - mkdir, rename, dirname, basename, @@ -222,8 +219,7 @@ def store(self, istream): if tmp_path: obj_path = self.db_path(self.object_path(hexsha)) obj_dir = dirname(obj_path) - if not isdir(obj_dir): - mkdir(obj_dir) + os.makedirs(obj_dir, exist_ok=True) # END handle destination directory # rename onto existing doesn't work on NTFS if isfile(obj_path): From 9d126ce78e69fb8aba28ae89adde69298a3066a4 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 04:33:18 -0400 Subject: [PATCH 132/158] Don't cancel other jobs from the 3.12 job failing Because 3.12 is still a release candidate and if tests fail for it then one would always want to know if/how other versions also fail. This also allows actions/setup-python to install a prerelease for 3.12 only, not for other releases. --- .github/workflows/pythonpackage.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index efa04a9..c59e618 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -16,6 +16,11 @@ jobs: strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + include: + - experimental: false + - python-version: "3.12" + experimental: true + continue-on-error: ${{ matrix.experimental }} steps: - uses: actions/checkout@v4 @@ -25,7 +30,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - allow-prereleases: true + allow-prereleases: ${{ matrix.experimental }} - name: Install project and dependencies run: | python -m pip install --upgrade pip From 919d3cce57d0a3c398dc4ddbacbbd23942a6ff4c Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 04:17:52 -0400 Subject: [PATCH 133/158] Use actions/checkout feature to fetch all commits Setting the "fetch-depth" to a positive value fetches that many commits back (and the default value is 1), but setting it to 0 fetches all commits, as in a (deep) normal fetch. --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index efa04a9..14c7ecb 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 1000 + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: From bd21ed46addf55fea5059d44e0477a785f4a664f Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Mon, 11 Sep 2023 05:18:27 -0400 Subject: [PATCH 134/158] Revert "Drop support for EOL Python 3.7" This brings back Python 3.7 support (allowing it to be installed on 3.7, and testing on 3.7 on CI), even though 3.7 is end-of-life, because support for 3.7 is not being dropped by GitPython yet, and there is value in keeping the version ranges supported by GitPython and gitdb consistent. This reverts commit 875acb45cd8e9353c1911717bd2cd05b3e40ed05. --- .github/workflows/pythonpackage.yml | 2 +- setup.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3d06b4a..6849763 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] include: - experimental: false - python-version: "3.12" diff --git a/setup.py b/setup.py index 5734122..61b5727 100755 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ zip_safe=False, install_requires=['smmap>=3.0.1,<6'], long_description="""GitDB is a pure-Python git object database""", - python_requires='>=3.8', + python_requires='>=3.7', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ "Development Status :: 5 - Production/Stable", @@ -35,6 +35,7 @@ "Operating System :: MacOS :: MacOS X", "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", From e0769d1ed2d9044d7523c2eb2f8a0d44a90deb9e Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 17 Sep 2023 08:36:34 -0400 Subject: [PATCH 135/158] Fix top-of-file license URLs here in gitdb too This is the gitdb part of the fix for the top-of-file license URLs that have come to point to a page about a related but different license from the one GitPython and gitdb are (intended to be) offered under. See https://github.com/gitpython-developers/GitPython/pull/1662 for details about the problem and how it came about. --- gitdb/__init__.py | 2 +- gitdb/base.py | 2 +- gitdb/db/__init__.py | 2 +- gitdb/db/base.py | 2 +- gitdb/db/git.py | 2 +- gitdb/db/loose.py | 2 +- gitdb/db/mem.py | 2 +- gitdb/db/pack.py | 2 +- gitdb/db/ref.py | 2 +- gitdb/exc.py | 2 +- gitdb/fun.py | 2 +- gitdb/pack.py | 6 +++--- gitdb/stream.py | 10 +++++----- gitdb/test/__init__.py | 2 +- gitdb/test/db/__init__.py | 2 +- gitdb/test/db/lib.py | 2 +- gitdb/test/db/test_git.py | 2 +- gitdb/test/db/test_loose.py | 2 +- gitdb/test/db/test_mem.py | 2 +- gitdb/test/db/test_pack.py | 2 +- gitdb/test/db/test_ref.py | 2 +- gitdb/test/lib.py | 2 +- gitdb/test/performance/lib.py | 2 +- gitdb/test/performance/test_pack.py | 2 +- gitdb/test/performance/test_pack_streaming.py | 2 +- gitdb/test/performance/test_stream.py | 2 +- gitdb/test/test_base.py | 2 +- gitdb/test/test_example.py | 2 +- gitdb/test/test_pack.py | 4 ++-- gitdb/test/test_stream.py | 2 +- gitdb/test/test_util.py | 2 +- gitdb/typ.py | 2 +- gitdb/util.py | 2 +- 33 files changed, 40 insertions(+), 40 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index b777632..2fb3f7e 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Initialize the object database module""" import sys diff --git a/gitdb/base.py b/gitdb/base.py index 42e71d0..9a23a4f 100644 --- a/gitdb/base.py +++ b/gitdb/base.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Module with basic data structures - they are designed to be lightweight and fast""" from gitdb.util import bin_to_hex diff --git a/gitdb/db/__init__.py b/gitdb/db/__init__.py index 0a2a46a..20fd228 100644 --- a/gitdb/db/__init__.py +++ b/gitdb/db/__init__.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ from gitdb.db.base import * from gitdb.db.loose import * diff --git a/gitdb/db/base.py b/gitdb/db/base.py index e89052e..7312fe0 100644 --- a/gitdb/db/base.py +++ b/gitdb/db/base.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Contains implementations of database retrieveing objects""" from gitdb.util import ( join, diff --git a/gitdb/db/git.py b/gitdb/db/git.py index e2cb468..a1ed142 100644 --- a/gitdb/db/git.py +++ b/gitdb/db/git.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ from gitdb.db.base import ( CompoundDB, ObjectDBW, diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 7ea6fef..256fec9 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ from gitdb.db.base import ( FileDBBase, ObjectDBR, diff --git a/gitdb/db/mem.py b/gitdb/db/mem.py index 1b954cb..d4772fd 100644 --- a/gitdb/db/mem.py +++ b/gitdb/db/mem.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Contains the MemoryDatabase implementation""" from gitdb.db.loose import LooseObjectDB from gitdb.db.base import ( diff --git a/gitdb/db/pack.py b/gitdb/db/pack.py index 1ce786b..274ea59 100644 --- a/gitdb/db/pack.py +++ b/gitdb/db/pack.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Module containing a database to deal with packs""" from gitdb.db.base import ( FileDBBase, diff --git a/gitdb/db/ref.py b/gitdb/db/ref.py index 6bb2a64..bd30156 100644 --- a/gitdb/db/ref.py +++ b/gitdb/db/ref.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ import codecs from gitdb.db.base import ( CompoundDB, diff --git a/gitdb/exc.py b/gitdb/exc.py index 947e5d8..7397037 100644 --- a/gitdb/exc.py +++ b/gitdb/exc.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Module with common exceptions""" from gitdb.util import to_hex_sha diff --git a/gitdb/fun.py b/gitdb/fun.py index a4454de..a272e5c 100644 --- a/gitdb/fun.py +++ b/gitdb/fun.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Contains basic c-functions which usually contain performance critical code Keeping this code separate from the beginning makes it easier to out-source it into c later, if required""" diff --git a/gitdb/pack.py b/gitdb/pack.py index cabce4c..e559e11 100644 --- a/gitdb/pack.py +++ b/gitdb/pack.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Contains PackIndexFile and PackFile implementations""" import zlib @@ -263,7 +263,7 @@ def __init__(self, indexpath): def close(self): mman.force_map_handle_removal_win(self._indexpath) self._cursor = None - + def _set_cache_(self, attr): if attr == "_packfile_checksum": self._packfile_checksum = self._cursor.map()[-40:-20] @@ -528,7 +528,7 @@ def __init__(self, packpath): def close(self): mman.force_map_handle_removal_win(self._packpath) self._cursor = None - + def _set_cache_(self, attr): # we fill the whole cache, whichever attribute gets queried first self._cursor = mman.make_cursor(self._packpath).use_region() diff --git a/gitdb/stream.py b/gitdb/stream.py index 1b5426f..1e0be84 100644 --- a/gitdb/stream.py +++ b/gitdb/stream.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO @@ -140,7 +140,7 @@ def data(self): def close(self): """Close our underlying stream of compressed bytes if this was allowed during initialization :return: True if we closed the underlying stream - :note: can be called safely + :note: can be called safely """ if self._close: if hasattr(self._m, 'close'): @@ -287,11 +287,11 @@ def read(self, size=-1): # if we hit the end of the stream # NOTE: Behavior changed in PY2.7 onward, which requires special handling to make the tests work properly. # They are thorough, and I assume it is truly working. - # Why is this logic as convoluted as it is ? Please look at the table in + # Why is this logic as convoluted as it is ? Please look at the table in # https://github.com/gitpython-developers/gitdb/issues/19 to learn about the test-results. # Basically, on py2.6, you want to use branch 1, whereas on all other python version, the second branch - # will be the one that works. - # However, the zlib VERSIONs as well as the platform check is used to further match the entries in the + # will be the one that works. + # However, the zlib VERSIONs as well as the platform check is used to further match the entries in the # table in the github issue. This is it ... it was the only way I could make this work everywhere. # IT's CERTAINLY GOING TO BITE US IN THE FUTURE ... . if getattr(zlib, 'ZLIB_RUNTIME_VERSION', zlib.ZLIB_VERSION) in ('1.2.7', '1.2.5') and not sys.platform == 'darwin': diff --git a/gitdb/test/__init__.py b/gitdb/test/__init__.py index 8a681e4..03bd406 100644 --- a/gitdb/test/__init__.py +++ b/gitdb/test/__init__.py @@ -1,4 +1,4 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ diff --git a/gitdb/test/db/__init__.py b/gitdb/test/db/__init__.py index 8a681e4..03bd406 100644 --- a/gitdb/test/db/__init__.py +++ b/gitdb/test/db/__init__.py @@ -1,4 +1,4 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ diff --git a/gitdb/test/db/lib.py b/gitdb/test/db/lib.py index b38f1d5..408dd8c 100644 --- a/gitdb/test/db/lib.py +++ b/gitdb/test/db/lib.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Base classes for object db testing""" from gitdb.test.lib import ( with_rw_directory, diff --git a/gitdb/test/db/test_git.py b/gitdb/test/db/test_git.py index 6ecf7d7..73ac1a0 100644 --- a/gitdb/test/db/test_git.py +++ b/gitdb/test/db/test_git.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ import os from gitdb.test.db.lib import ( TestDBBase, diff --git a/gitdb/test/db/test_loose.py b/gitdb/test/db/test_loose.py index 8cc660b..295e2ee 100644 --- a/gitdb/test/db/test_loose.py +++ b/gitdb/test/db/test_loose.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ from gitdb.test.db.lib import ( TestDBBase, with_rw_directory diff --git a/gitdb/test/db/test_mem.py b/gitdb/test/db/test_mem.py index eb563c0..882e54f 100644 --- a/gitdb/test/db/test_mem.py +++ b/gitdb/test/db/test_mem.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ from gitdb.test.db.lib import ( TestDBBase, with_rw_directory diff --git a/gitdb/test/db/test_pack.py b/gitdb/test/db/test_pack.py index 4539f42..bd07906 100644 --- a/gitdb/test/db/test_pack.py +++ b/gitdb/test/db/test_pack.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ from gitdb.test.db.lib import ( TestDBBase, with_rw_directory, diff --git a/gitdb/test/db/test_ref.py b/gitdb/test/db/test_ref.py index 664aa54..0816e64 100644 --- a/gitdb/test/db/test_ref.py +++ b/gitdb/test/db/test_ref.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ from gitdb.test.db.lib import ( TestDBBase, with_rw_directory, diff --git a/gitdb/test/lib.py b/gitdb/test/lib.py index da59d3b..8e60234 100644 --- a/gitdb/test/lib.py +++ b/gitdb/test/lib.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Utilities used in ODB testing""" from gitdb import OStream diff --git a/gitdb/test/performance/lib.py b/gitdb/test/performance/lib.py index fa4dd20..36916ed 100644 --- a/gitdb/test/performance/lib.py +++ b/gitdb/test/performance/lib.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Contains library functions""" from gitdb.test.lib import TestBase diff --git a/gitdb/test/performance/test_pack.py b/gitdb/test/performance/test_pack.py index f034baf..fc3d334 100644 --- a/gitdb/test/performance/test_pack.py +++ b/gitdb/test/performance/test_pack.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Performance tests for object store""" from gitdb.test.performance.lib import ( diff --git a/gitdb/test/performance/test_pack_streaming.py b/gitdb/test/performance/test_pack_streaming.py index db790f1..80c798b 100644 --- a/gitdb/test/performance/test_pack_streaming.py +++ b/gitdb/test/performance/test_pack_streaming.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Specific test for pack streams only""" from gitdb.test.performance.lib import ( diff --git a/gitdb/test/performance/test_stream.py b/gitdb/test/performance/test_stream.py index 91dc891..fb10871 100644 --- a/gitdb/test/performance/test_stream.py +++ b/gitdb/test/performance/test_stream.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Performance data streaming performance""" from gitdb.test.performance.lib import TestBigRepoR diff --git a/gitdb/test/test_base.py b/gitdb/test/test_base.py index 519cdfd..8fc9e35 100644 --- a/gitdb/test/test_base.py +++ b/gitdb/test/test_base.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Test for object db""" from gitdb.test.lib import ( TestBase, diff --git a/gitdb/test/test_example.py b/gitdb/test/test_example.py index cc4d40d..3b4c908 100644 --- a/gitdb/test/test_example.py +++ b/gitdb/test/test_example.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Module with examples from the tutorial section of the docs""" import os from gitdb.test.lib import TestBase diff --git a/gitdb/test/test_pack.py b/gitdb/test/test_pack.py index f946197..e723482 100644 --- a/gitdb/test/test_pack.py +++ b/gitdb/test/test_pack.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Test everything about packs reading and writing""" from gitdb.test.lib import ( TestBase, @@ -242,7 +242,7 @@ def rewind_streams(): # END for each info assert count == len(pack_objs) entity.close() - + def test_pack_64(self): # TODO: hex-edit a pack helping us to verify that we can handle 64 byte offsets # of course without really needing such a huge pack diff --git a/gitdb/test/test_stream.py b/gitdb/test/test_stream.py index 5e2e1ba..1e7e941 100644 --- a/gitdb/test/test_stream.py +++ b/gitdb/test/test_stream.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Test for object db""" from gitdb.test.lib import ( diff --git a/gitdb/test/test_util.py b/gitdb/test/test_util.py index 3b3165d..166b33c 100644 --- a/gitdb/test/test_util.py +++ b/gitdb/test/test_util.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Test for object db""" import tempfile import os diff --git a/gitdb/typ.py b/gitdb/typ.py index 98d15f3..314db50 100644 --- a/gitdb/typ.py +++ b/gitdb/typ.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ """Module containing information about types known to the database""" str_blob_type = b'blob' diff --git a/gitdb/util.py b/gitdb/util.py index 3151c06..bb6d879 100644 --- a/gitdb/util.py +++ b/gitdb/util.py @@ -1,7 +1,7 @@ # Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors # # This module is part of GitDB and is released under -# the New BSD License: http://www.opensource.org/licenses/bsd-license.php +# the New BSD License: https://opensource.org/license/bsd-3-clause/ import binascii import os import mmap From cd8da415e119451e195903576a8ff22cf60ae47b Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 12:42:23 -0400 Subject: [PATCH 136/158] Run CI on all branches This makes it easier to test changes to CI without/before a PR. + Fix a small YAML indentation style inconsistency. --- .github/workflows/pythonpackage.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6849763..6d819ff 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -3,11 +3,7 @@ name: Python package -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -17,9 +13,9 @@ jobs: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] include: - - experimental: false - - python-version: "3.12" - experimental: true + - experimental: false + - python-version: "3.12" + experimental: true continue-on-error: ${{ matrix.experimental }} steps: From 00bbc44eff7be0b43a882eadf54c2bbb2f9000a0 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Tue, 3 Oct 2023 12:44:38 -0400 Subject: [PATCH 137/158] No longer treat 3.12 as experimental on CI Since Python 3.12.0 stable has been released, as well as now being available via setup-python, per: https://github.com/actions/python-versions/blob/main/versions-manifest.json The main practical effect of this is that continue-on-error is no longer set to true for 3.12, so 3.12 is no longer special-cased to refrain from cancelling other test jobs when its test job fails. Another effect is that 3.12 can longer be selected as a prerelease. --- .github/workflows/pythonpackage.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6d819ff..73b3902 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -14,8 +14,6 @@ jobs: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] include: - experimental: false - - python-version: "3.12" - experimental: true continue-on-error: ${{ matrix.experimental }} steps: From 70098c9c99db872ac98a6b5c7a591a42cadf049f Mon Sep 17 00:00:00 2001 From: DeflateAwning <11021263+DeflateAwning@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:22:44 -0600 Subject: [PATCH 138/158] Add __all__ to exc for linting --- gitdb/exc.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gitdb/exc.py b/gitdb/exc.py index 7397037..752dafd 100644 --- a/gitdb/exc.py +++ b/gitdb/exc.py @@ -5,6 +5,17 @@ """Module with common exceptions""" from gitdb.util import to_hex_sha +__all__ = [ + 'AmbiguousObjectName', + 'BadName', + 'BadObject', + 'BadObjectType', + 'InvalidDBRoot', + 'ODBError', + 'ParseError', + 'UnsupportedOperation', + 'to_hex_sha', +] class ODBError(Exception): """All errors thrown by the object database""" From 12db86cefa90e8fc1c8df8f5c6b4c8b7a232d773 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 13 Oct 2023 04:06:47 -0400 Subject: [PATCH 139/158] Have Dependabot update smmap submodule dependency This makes Dependabot open version update PRs for submodules (which here is just smmap), as well as GitHub Actions. This is like https://github.com/gitpython-developers/GitPython/pull/1702. --- .github/dependabot.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 203f3c8..5acde1a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,4 +3,9 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "weekly" + +- package-ecosystem: "gitsubmodule" + directory: "/" + schedule: + interval: "monthly" From 8e613962153baad0431358dda03d9fd942cef616 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 08:17:45 +0000 Subject: [PATCH 140/158] Bump gitdb/ext/smmap from `334ef84` to `f1ace75` Bumps [gitdb/ext/smmap](https://github.com/gitpython-developers/smmap) from `334ef84` to `f1ace75`. - [Commits](https://github.com/gitpython-developers/smmap/compare/334ef84a05c953ed5dbec7b9c6d4310879eeab5a...f1ace75be355fdec927793e462b9b12bf6ec9520) --- updated-dependencies: - dependency-name: gitdb/ext/smmap dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- gitdb/ext/smmap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 334ef84..f1ace75 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 334ef84a05c953ed5dbec7b9c6d4310879eeab5a +Subproject commit f1ace75be355fdec927793e462b9b12bf6ec9520 From 2057ae67b85fb9925efbd0f00f44413e506e286c Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 20 Oct 2023 09:37:58 +0200 Subject: [PATCH 141/158] bump versions to prepare for next release --- doc/source/changes.rst | 6 ++++++ gitdb/__init__.py | 2 +- gitdb/ext/smmap | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 7d85ea5..0b8de13 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ######### +****** +4.0.11 +****** + +- various improvements - please see the release on GitHub for details. + ****** 4.0.10 ****** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 2fb3f7e..803a428 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -28,7 +28,7 @@ def _init_externals(): __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 10) +version_info = (4, 0, 11) __version__ = '.'.join(str(i) for i in version_info) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index f1ace75..256c5a2 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit f1ace75be355fdec927793e462b9b12bf6ec9520 +Subproject commit 256c5a21de2d14aca02c9689d7d63f78c4e0ef61 diff --git a/setup.py b/setup.py index 61b5727..f67f7a5 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 10) +version_info = (4, 0, 11) __version__ = '.'.join(str(i) for i in version_info) setup( From 3d3e9572dc452fea53d328c101b3d1440bbefe40 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Fri, 20 Oct 2023 09:42:19 +0200 Subject: [PATCH 142/158] fix makefile to allow building packages with current python version It all breaks all the time, it's just like that. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7aa5a71..a0a2d0e 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ release:: clean force_release:: clean git push --tags - python3 setup.py sdist bdist_wheel + python3 -m build --sdist --wheel twine upload dist/* doc:: From 965d2d36703a60f610e4b4a3fb1b86fe244d63d5 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 20 Oct 2023 06:46:10 -0400 Subject: [PATCH 143/158] Never add a vendored smmap directory to sys.path This removes the logic that appended the git submodule directory for smmap to sys.path under most circumstances when the version of gitdb was not from PyPI. Now gitdb does not modify sys.path. See https://github.com/gitpython-developers/GitPython/issues/1717 and https://github.com/gitpython-developers/GitPython/pull/1720 for context. This change is roughly equivalent to the change to GitPython, though as noted the behavior being eliminated is subtly different here and there. --- gitdb/__init__.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 803a428..9b77e9f 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -4,34 +4,12 @@ # the New BSD License: https://opensource.org/license/bsd-3-clause/ """Initialize the object database module""" -import sys -import os - -#{ Initialization - - -def _init_externals(): - """Initialize external projects by putting them into the path""" - if 'PYOXIDIZER' not in os.environ: - where = os.path.join(os.path.dirname(__file__), 'ext', 'smmap') - if os.path.exists(where): - sys.path.append(where) - - import smmap - del smmap - # END handle imports - -#} END initialization - -_init_externals() - __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" version_info = (4, 0, 11) __version__ = '.'.join(str(i) for i in version_info) - # default imports from gitdb.base import * from gitdb.db import * From dfbfb12beee6b2c61cb02f193fabc427e0a949f6 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 20 Oct 2023 07:24:33 -0400 Subject: [PATCH 144/158] Revise and update the readme Changes worth mentioning: - Format commands as code blocks instead of blockquotes. (This is particularly useful for the submodule update step, whose lines were inadvertently concatenated, but it also improves appearance overall.) - Mention smmap as a requirement. (But also that it doesn't need to be separately installed.) - Mention that gitdb-speedups is not currently maintained. - No longer say gitdb has source code in gitorious. (Since that site no longer exists.) - Call GitPython "GitPython" rather than "git-python". - Replace the old git-python Google Groups link with a link to the Discussions page on the GitHub repository for GitPython. (This seems like the closest currently available resource.) --- README.rst | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 29c70f7..61ce28b 100644 --- a/README.rst +++ b/README.rst @@ -16,34 +16,38 @@ Installation :target: https://readthedocs.org/projects/gitdb/?badge=latest :alt: Documentation Status -From `PyPI `_ +From `PyPI `_:: pip install gitdb SPEEDUPS ======== -If you want to go up to 20% faster, you can install gitdb-speedups with: +If you want to go up to 20% faster, you can install gitdb-speedups with:: pip install gitdb-speedups +However, please note that gitdb-speedups is not currently maintained. + REQUIREMENTS ============ +* smmap - declared as a dependency, automatically installed * pytest - for running the tests SOURCE ====== -The source is available in a git repository at gitorious and github: + +The source is available in a git repository on GitHub: https://github.com/gitpython-developers/gitdb -Once the clone is complete, please be sure to initialize the submodules using +Once the clone is complete, please be sure to initialize the submodule using:: cd gitdb git submodule update --init -Run the tests with +Run the tests with:: pytest @@ -53,13 +57,13 @@ DEVELOPMENT .. image:: https://github.com/gitpython-developers/gitdb/workflows/Python%20package/badge.svg :target: https://github.com/gitpython-developers/gitdb/actions -The library is considered mature, and not under active development. It's primary (known) use is in git-python. +The library is considered mature, and not under active development. Its primary (known) use is in GitPython. INFRASTRUCTURE ============== -* Mailing List - * http://groups.google.com/group/git-python +* Discussions + * https://github.com/gitpython-developers/GitPython/discussions * Issue Tracker * https://github.com/gitpython-developers/gitdb/issues From e998429c01f928da7ff7c922ba3f1249c43ff569 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Fri, 20 Oct 2023 07:47:10 -0400 Subject: [PATCH 145/158] Set Dependabot submodule update cadence to weekly This changes it from monthly to weekly. See #99 and https://github.com/gitpython-developers/GitPython/pull/1702#issuecomment-1761182333 for context. --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5acde1a..2fe73ca 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,4 @@ updates: - package-ecosystem: "gitsubmodule" directory: "/" schedule: - interval: "monthly" + interval: "weekly" From 24ecf58262eb2e76906689dbc4e28397f4f628dc Mon Sep 17 00:00:00 2001 From: Antoine C Date: Fri, 8 Dec 2023 16:58:24 +0100 Subject: [PATCH 146/158] fix #101 --- gitdb/test/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitdb/test/test_base.py b/gitdb/test/test_base.py index 8fc9e35..17906c9 100644 --- a/gitdb/test/test_base.py +++ b/gitdb/test/test_base.py @@ -73,7 +73,7 @@ def test_streams(self): # test deltapackstream dpostream = ODeltaPackStream(*(dpinfo + (stream, ))) - dpostream.stream is stream + assert dpostream.stream is stream dpostream.read(5) stream._assert() assert stream.bytes == 5 @@ -92,7 +92,7 @@ def test_streams(self): assert istream.size == s istream.size = s * 2 - istream.size == s * 2 + assert istream.size == s * 2 assert istream.type == str_blob_type istream.type = "something" assert istream.type == "something" From 86402e67e7d999b2b2665dc1029c5e1ccd3ada35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:03:07 +0000 Subject: [PATCH 147/158] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 73b3902..ec7550d 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -21,7 +21,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: ${{ matrix.experimental }} From 5aeb6e073ebe007d43695976eccdcca64568d025 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:39:28 +0000 Subject: [PATCH 148/158] Bump gitdb/ext/smmap from `256c5a2` to `04dd210` Bumps [gitdb/ext/smmap](https://github.com/gitpython-developers/smmap) from `256c5a2` to `04dd210`. - [Commits](https://github.com/gitpython-developers/smmap/compare/256c5a21de2d14aca02c9689d7d63f78c4e0ef61...04dd2103ee6e0b7483889e5feda25053c6df2b52) --- updated-dependencies: - dependency-name: gitdb/ext/smmap dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- gitdb/ext/smmap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 256c5a2..04dd210 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 256c5a21de2d14aca02c9689d7d63f78c4e0ef61 +Subproject commit 04dd2103ee6e0b7483889e5feda25053c6df2b52 From d50b2e3245f472637c6b86722d6dd969fb4c7183 Mon Sep 17 00:00:00 2001 From: Almaz Ilaletdinov Date: Sun, 9 Jun 2024 14:00:05 +0300 Subject: [PATCH 149/158] Use contextlib.suppress instead of except: pass --- gitdb/db/loose.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 256fec9..87cde86 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -2,6 +2,8 @@ # # This module is part of GitDB and is released under # the New BSD License: https://opensource.org/license/bsd-3-clause/ +from contextlib import suppress + from gitdb.db.base import ( FileDBBase, ObjectDBR, @@ -90,10 +92,8 @@ def readable_db_object_path(self, hexsha): """ :return: readable object path to the object identified by hexsha :raise BadObject: If the object file does not exist""" - try: + with suppress(KeyError): return self._hexsha_to_file[hexsha] - except KeyError: - pass # END ignore cache misses # try filesystem From 5bc95043792c2412b05263fb4bfca67d7923645c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 9 Oct 2024 01:01:55 -0600 Subject: [PATCH 150/158] Add support for Python 3.13 --- .github/workflows/pythonpackage.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 73b3902..8e0ff8e 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] include: - experimental: false continue-on-error: ${{ matrix.experimental }} diff --git a/setup.py b/setup.py index f67f7a5..51065c9 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", ] ) From b38cbc43354523ffcd59a58c5a3aded054bd4442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez-Mondrag=C3=B3n?= Date: Wed, 9 Oct 2024 01:05:50 -0600 Subject: [PATCH 151/158] Use older ubuntu to get Python 3.7 --- .github/workflows/pythonpackage.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 8e0ff8e..40e1c0a 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -8,12 +8,17 @@ on: [push, pull_request, workflow_dispatch] jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + os: [ubuntu-latest] + experimental: [false] include: - - experimental: false + - python-version: "3.7" + os: ubuntu-22.04 + experimental: false continue-on-error: ${{ matrix.experimental }} steps: From 74a0eabbc03209593ea1562498802359ae8a3db7 Mon Sep 17 00:00:00 2001 From: Jonathan Dekhtiar Date: Mon, 23 Dec 2024 23:02:58 -0500 Subject: [PATCH 152/158] Potential Race Condition Fix - OS Rename & Chmod --- gitdb/db/loose.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 87cde86..ccefe40 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -54,6 +54,7 @@ import tempfile import os import sys +import time __all__ = ('LooseObjectDB', ) @@ -205,7 +206,7 @@ def store(self, istream): # END assure target stream is closed except: if tmp_path: - os.remove(tmp_path) + remove(tmp_path) raise # END assure tmpfile removal on error @@ -228,9 +229,25 @@ def store(self, istream): rename(tmp_path, obj_path) # end rename only if needed - # make sure its readable for all ! It started out as rw-- tmp file - # but needs to be rwrr - chmod(obj_path, self.new_objects_mode) + # Ensure rename is actually done and file is stable + # Retry up to 14 times - exponential wait & retry in ms. + # The total maximum wait time is 1000ms, which should be vastly enough for the + # OS to return and commit the file to disk. + for exp_backoff_ms in [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 181]: + with suppress(PermissionError): + # make sure its readable for all ! It started out as rw-- tmp file + # but needs to be rwrr + chmod(obj_path, self.new_objects_mode) + break + time.sleep(exp_backoff_ms / 1000.0) + else: + raise PermissionError( + "Impossible to apply `chmod` to file {}".format(obj_path) + ) + + # Cleanup + with suppress(FileNotFoundError): + remove(tmp_path) # END handle dry_run istream.binsha = hex_to_bin(hexsha) From b71e2730c3dcab148816f0193a45550ef0a38c79 Mon Sep 17 00:00:00 2001 From: Jonathan DEKHTIAR Date: Sun, 29 Dec 2024 19:44:26 -0500 Subject: [PATCH 153/158] Update gitdb/db/loose.py Co-authored-by: Sebastian Thiel --- gitdb/db/loose.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index ccefe40..03d387e 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -245,9 +245,6 @@ def store(self, istream): "Impossible to apply `chmod` to file {}".format(obj_path) ) - # Cleanup - with suppress(FileNotFoundError): - remove(tmp_path) # END handle dry_run istream.binsha = hex_to_bin(hexsha) From 104138c742a56d85bd2cb2cd8a9f90336daa5483 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 2 Jan 2025 08:15:19 +0100 Subject: [PATCH 154/158] bump patch level to prepare for next release --- doc/source/changes.rst | 6 ++++++ gitdb/__init__.py | 2 +- gitdb/ext/smmap | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 0b8de13..b4340e4 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,12 @@ Changelog ######### +****** +4.0.12 +****** + +- various improvements - please see the release on GitHub for details. + ****** 4.0.11 ****** diff --git a/gitdb/__init__.py b/gitdb/__init__.py index 9b77e9f..1fb7df8 100644 --- a/gitdb/__init__.py +++ b/gitdb/__init__.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 11) +version_info = (4, 0, 12) __version__ = '.'.join(str(i) for i in version_info) # default imports diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index 04dd210..f31bfa3 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit 04dd2103ee6e0b7483889e5feda25053c6df2b52 +Subproject commit f31bfa378c8840d38d31e7e11ef2b84f191a491e diff --git a/setup.py b/setup.py index 51065c9..3a91543 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ __author__ = "Sebastian Thiel" __contact__ = "byronimo@gmail.com" __homepage__ = "https://github.com/gitpython-developers/gitdb" -version_info = (4, 0, 11) +version_info = (4, 0, 12) __version__ = '.'.join(str(i) for i in version_info) setup( From 775cfe8299ea5474f605935469359a9d1cdb49dc Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 2 Jan 2025 08:20:58 +0100 Subject: [PATCH 155/158] update scripts to allow release (copied from smmap) --- Makefile | 42 +++++++----------------------------------- build-release.sh | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 35 deletions(-) create mode 100755 build-release.sh diff --git a/Makefile b/Makefile index a0a2d0e..20436bb 100644 --- a/Makefile +++ b/Makefile @@ -1,40 +1,12 @@ -PYTHON = python3 -SETUP = $(PYTHON) setup.py -TESTFLAGS = +.PHONY: all clean release force_release -all:: +all: @grep -Ee '^[a-z].*:' Makefile | cut -d: -f1 | grep -vF all -release:: clean - # Check if latest tag is the current head we're releasing - echo "Latest tag = $$(git tag | sort -nr | head -n1)" - echo "HEAD SHA = $$(git rev-parse head)" - echo "Latest tag SHA = $$(git tag | sort -nr | head -n1 | xargs git rev-parse)" - @test "$$(git rev-parse head)" = "$$(git tag | sort -nr | head -n1 | xargs git rev-parse)" - make force_release +clean: + rm -rf build/ dist/ .eggs/ .tox/ -force_release:: clean - git push --tags - python3 -m build --sdist --wheel +force_release: clean + ./build-release.sh twine upload dist/* - -doc:: - make -C doc/ html - -build:: - $(SETUP) build - $(SETUP) build_ext -i - -build_ext:: - $(SETUP) build_ext -i - -install:: - $(SETUP) install - -clean:: - $(SETUP) clean --all - rm -f *.so - -coverage:: build - PYTHONPATH=. $(PYTHON) -m pytest --cov=gitdb gitdb - + git push --tags origin master diff --git a/build-release.sh b/build-release.sh new file mode 100755 index 0000000..5840e44 --- /dev/null +++ b/build-release.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# This script builds a release. If run in a venv, it auto-installs its tools. +# You may want to run "make release" instead of running this script directly. + +set -eEu + +function release_with() { + $1 -m build --sdist --wheel +} + +if test -n "${VIRTUAL_ENV:-}"; then + deps=(build twine) # Install twine along with build, as we need it later. + echo "Virtual environment detected. Adding packages: ${deps[*]}" + pip install --quiet --upgrade "${deps[@]}" + echo 'Starting the build.' + release_with python +else + function suggest_venv() { + venv_cmd='python -m venv env && source env/bin/activate' + printf "HELP: To avoid this error, use a virtual-env with '%s' instead.\n" "$venv_cmd" + } + trap suggest_venv ERR # This keeps the original exit (error) code. + echo 'Starting the build.' + release_with python3 # Outside a venv, use python3. +fi From 26209528a0303e47c88c174184adbf25d206a824 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 5 Jan 2025 03:21:33 -0500 Subject: [PATCH 156/158] Add SECURITY.md, referencing GitPython's Along with https://github.com/gitpython-developers/smmap/pull/59 and a forthcoming related PR in GitPython, this will fix #116. --- SECURITY.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..95389ff --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +See [GitPython](https://github.com/gitpython-developers/GitPython/blob/main/SECURITY.md). Vulnerabilities found in `gitdb` can be reported there. From 4fe56572894f9668c1ffd0808c96aed27c65e584 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 10:38:13 +0000 Subject: [PATCH 157/158] Bump gitdb/ext/smmap from `f31bfa3` to `8f82e6c` Bumps [gitdb/ext/smmap](https://github.com/gitpython-developers/smmap) from `f31bfa3` to `8f82e6c`. - [Release notes](https://github.com/gitpython-developers/smmap/releases) - [Commits](https://github.com/gitpython-developers/smmap/compare/f31bfa378c8840d38d31e7e11ef2b84f191a491e...8f82e6c19661f9b735cc55cc89031a189e408894) --- updated-dependencies: - dependency-name: gitdb/ext/smmap dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- gitdb/ext/smmap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitdb/ext/smmap b/gitdb/ext/smmap index f31bfa3..8f82e6c 160000 --- a/gitdb/ext/smmap +++ b/gitdb/ext/smmap @@ -1 +1 @@ -Subproject commit f31bfa378c8840d38d31e7e11ef2b84f191a491e +Subproject commit 8f82e6c19661f9b735cc55cc89031a189e408894 From b4fd74ce8e28c372c511db2e0a491fa8b67c93f4 Mon Sep 17 00:00:00 2001 From: Eliah Kagan Date: Sun, 26 Jan 2025 11:51:11 -0500 Subject: [PATCH 158/158] Improve description of backoff sequence in db.loose The sequence of backoff wait times used in `gitdb.db.loose` is quadratic rather than exponential, as discussed in: https://github.com/gitpython-developers/gitdb/pull/115#discussion_r1903215598 This corrects the variable name by making it more general, and the comment by having it explicitly describe the backoff as quadratic. This is conceptually related to GitoxideLabs/gitoxide#1815, but this is a non-breaking change, as no interfaces are affected: only a local variable and comment. --- gitdb/db/loose.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gitdb/db/loose.py b/gitdb/db/loose.py index 03d387e..e6765cd 100644 --- a/gitdb/db/loose.py +++ b/gitdb/db/loose.py @@ -230,16 +230,16 @@ def store(self, istream): # end rename only if needed # Ensure rename is actually done and file is stable - # Retry up to 14 times - exponential wait & retry in ms. + # Retry up to 14 times - quadratic wait & retry in ms. # The total maximum wait time is 1000ms, which should be vastly enough for the # OS to return and commit the file to disk. - for exp_backoff_ms in [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 181]: + for backoff_ms in [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 181]: with suppress(PermissionError): # make sure its readable for all ! It started out as rw-- tmp file # but needs to be rwrr chmod(obj_path, self.new_objects_mode) break - time.sleep(exp_backoff_ms / 1000.0) + time.sleep(backoff_ms / 1000.0) else: raise PermissionError( "Impossible to apply `chmod` to file {}".format(obj_path)