From b008e6e1c31416b08fc8703609eb7f7894992133 Mon Sep 17 00:00:00 2001 From: Daniel Stonier Date: Sun, 3 Sep 2023 15:11:17 +0000 Subject: [PATCH] [decorators] finally-style decorators and idioms --- .vscode/settings.json | 5 + CHANGELOG.rst | 2 +- docs/demos.rst | 16 +++ docs/dot/demo-eventually.dot | 17 +++ docs/dot/demo-finally-single-tick.dot | 17 +++ docs/dot/eventually.dot | 17 +++ docs/examples/eventually.py | 26 ++++ docs/images/finally_single_tick.png | Bin 0 -> 50795 bytes py_trees/behaviour.py | 2 +- py_trees/behaviours.py | 1 + py_trees/decorators.py | 77 ++++++++++++ py_trees/demos/__init__.py | 1 + py_trees/demos/eventually.py | 172 ++++++++++++++++++++++++++ py_trees/idioms.py | 38 +++++- pyproject.toml | 1 + tests/test_decorators.py | 37 ++++++ 16 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 docs/dot/demo-eventually.dot create mode 100644 docs/dot/demo-finally-single-tick.dot create mode 100644 docs/dot/eventually.dot create mode 100755 docs/examples/eventually.py create mode 100644 docs/images/finally_single_tick.png create mode 100644 py_trees/demos/eventually.py diff --git a/.vscode/settings.json b/.vscode/settings.json index b96bb6fa..49b2588c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,10 +12,15 @@ "behaviours", "bierner", "bungcip", + "epilog", + "graphviz", + "literalinclude", + "noodly", "omnilib", "py_trees", "pydot", "pypi", + "seealso", "ufmt", "usort" ] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f411867b..635bab5d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,7 @@ Release Notes Forthcoming ----------- -* ... +* [decorators] finally-style decorators and idioms, `#427 `_ 2.2.3 (2023-02-08) ------------------ diff --git a/docs/demos.rst b/docs/demos.rst index a0154cc4..9a7fb2e3 100644 --- a/docs/demos.rst +++ b/docs/demos.rst @@ -147,6 +147,22 @@ py-trees-demo-eternal-guard :linenos: :caption: py_trees/demos/eternal_guard.py +.. _py-trees-demo-eventually-program: + +py-trees-demo-eventually +------------------------ + +.. automodule:: py_trees.demos.eventually + :members: + :special-members: + :show-inheritance: + :synopsis: demo the finally-like decorator + +.. literalinclude:: ../py_trees/demos/eventually.py + :language: python + :linenos: + :caption: py_trees/demos/eventually.py + .. _py-trees-demo-logging-program: py-trees-demo-logging diff --git a/docs/dot/demo-eventually.dot b/docs/dot/demo-eventually.dot new file mode 100644 index 00000000..03436af5 --- /dev/null +++ b/docs/dot/demo-eventually.dot @@ -0,0 +1,17 @@ +digraph pastafarianism { +ordering=out; +graph [fontname="times-roman"]; +node [fontname="times-roman"]; +edge [fontname="times-roman"]; +root [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ root", shape=box, style=filled]; +SetFlagFalse [fillcolor=gray, fontcolor=black, fontsize=9, label=SetFlagFalse, shape=ellipse, style=filled]; +root -> SetFlagFalse; +Parallel [fillcolor=gold, fontcolor=black, fontsize=9, label="Parallel\nSuccessOnOne", shape=parallelogram, style=filled]; +root -> Parallel; +Counter [fillcolor=gray, fontcolor=black, fontsize=9, label=Counter, shape=ellipse, style=filled]; +Parallel -> Counter; +Eventually [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=Eventually, shape=ellipse, style=filled]; +Parallel -> Eventually; +SetFlagTrue [fillcolor=gray, fontcolor=black, fontsize=9, label=SetFlagTrue, shape=ellipse, style=filled]; +Eventually -> SetFlagTrue; +} diff --git a/docs/dot/demo-finally-single-tick.dot b/docs/dot/demo-finally-single-tick.dot new file mode 100644 index 00000000..12ee71f3 --- /dev/null +++ b/docs/dot/demo-finally-single-tick.dot @@ -0,0 +1,17 @@ +digraph pastafarianism { +ordering=out; +graph [fontname="times-roman"]; +node [fontname="times-roman"]; +edge [fontname="times-roman"]; +root [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ root", shape=box, style=filled]; +SetFlagFalse [fillcolor=gray, fontcolor=black, fontsize=9, label=SetFlagFalse, shape=ellipse, style=filled]; +root -> SetFlagFalse; +Parallel [fillcolor=gold, fontcolor=black, fontsize=9, label="Parallel\nSuccessOnOne", shape=parallelogram, style=filled]; +root -> Parallel; +Counter [fillcolor=gray, fontcolor=black, fontsize=9, label=Counter, shape=ellipse, style=filled]; +Parallel -> Counter; +Finally [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=Finally, shape=ellipse, style=filled]; +Parallel -> Finally; +SetFlagTrue [fillcolor=gray, fontcolor=black, fontsize=9, label=SetFlagTrue, shape=ellipse, style=filled]; +Finally -> SetFlagTrue; +} diff --git a/docs/dot/eventually.dot b/docs/dot/eventually.dot new file mode 100644 index 00000000..db1b529e --- /dev/null +++ b/docs/dot/eventually.dot @@ -0,0 +1,17 @@ +digraph pastafarianism { +ordering=out; +graph [fontname="times-roman"]; +node [fontname="times-roman"]; +edge [fontname="times-roman"]; +Parallel [fillcolor=gold, fontcolor=black, fontsize=9, label="Parallel\nSuccessOnOne", shape=parallelogram, style=filled]; +Worker [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Worker", shape=box, style=filled]; +Parallel -> Worker; +Glory [fillcolor=gray, fontcolor=black, fontsize=9, label=Glory, shape=ellipse, style=filled]; +Worker -> Glory; +Infamy [fillcolor=gray, fontcolor=black, fontsize=9, label=Infamy, shape=ellipse, style=filled]; +Worker -> Infamy; +Eventually [fillcolor=ghostwhite, fontcolor=black, fontsize=9, label=Eventually, shape=ellipse, style=filled]; +Parallel -> Eventually; +Colander [fillcolor=gray, fontcolor=black, fontsize=9, label=Colander, shape=ellipse, style=filled]; +Eventually -> Colander; +} diff --git a/docs/examples/eventually.py b/docs/examples/eventually.py new file mode 100755 index 00000000..92fbb66c --- /dev/null +++ b/docs/examples/eventually.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import py_trees + +if __name__ == "__main__": + root = py_trees + task_one = py_trees.behaviours.StatusQueue( + name="Glory", + queue=[ + py_trees.common.Status.RUNNING, + ], + eventually=py_trees.common.Status.SUCCESS, + ) + task_two = py_trees.behaviours.Success(name="Infamy") + worker = py_trees.composites.Sequence( + name="Worker", memory=True, children=[task_one, task_two] + ) + root = py_trees.idioms.eventually( + name="Parallel", + worker=worker, + eventually=py_trees.behaviours.Success("Colander"), + ) + py_trees.display.render_dot_tree( + root, py_trees.common.string_to_visibility_level("all") + ) diff --git a/docs/images/finally_single_tick.png b/docs/images/finally_single_tick.png new file mode 100644 index 0000000000000000000000000000000000000000..b8e5cdee7edaf125279b057e2fd107a39f0e2ae1 GIT binary patch literal 50795 zcmd432UJt-x;2WQT~r7~RGM_8BS?|sg{&pG#waqk`DzxN#jUe-#o@@B2|JkNaQoFP<0P3g|9hqp*bNbbCR z0oEcR`742h>E`wv zeRU`1dt!6))Rsrsk!4kxiYu2=w4{GO7b7*OyzP}gBlHw_z55e`AFp;|Z7bWOG0|68 z(>A{v1d<29Ck%_{_s`y*rv^E#3@f!W~TD;k(uujB6kJN^G{ zgD`}B`m#1WYnkTe;e#KI-?l$BkgoGx{6S%B`pn?X8&-H%B_SBrZx?)Nu*U*-G!G_P`>rR@urJEV51IRBZ*l*}Gg^=Vd-g=B1I5X) z5&rf_i_z?vA30GH>%gy*pu*EX`VjTU3#)Nx#isKR^5E_aK5wLB)8#rbD&kAD*{?LyLv&5?mW zt!^*0?%!23I6 zeA)R&@v}|v99cnTv-shhbJpr}Z&{fyd>NubFs|X89*N!g{m7nBP1d zjRIA@#38j|eEY25{2pgakm=`nRouSBu_xJ4qkn%)LxsnH2DTpes*3CBe!{>RNAfW5 zX`=(x@zLciMw0J~bPHbT)n}!Y)_XU)@6;rDlxUUo&zjS$_3+x$9-4_Z%n4(0I=1k9 z)wvDRnyJel9xW(A(1!X_Bj+BS^O^!q62`h77HUTV_Tx%@A~GJT#bpi8!2Ke6bejEj zbCe3xx>%E0xYOz^h_^CNK=hR%5>godK*$ch5nrdlVJ;i=(q7_H1C2s2IZXBlqf7YUt6%pA2g6LRN8R|)pH z7nP}K>f}NjeAhjyk1B9m{4-_E8tkHLD%BAUoNk9toW_2X{`hUS5?eIXoPehS4M&kh zNJ4uKGMRC_$nBQ;Vn+{MM=&_=S`N>`LR?S-SLv`~#Eu8Km>Nv^PS@;kP8f=DKrcB`sqx(sjTvH$`@k$}v>Or% zwVmp^wdI7{)z`I+{HwgJhpK{;`N%;tG7&mmW?#L&TpYJFrXn{(wkg4*Z$A^ZB!5le z?Hv%CZbl&YexZtz*<8RxSIN+%d4r_x_CvVYhO}|5`G(ps{z~*d54?ah=SNS1(r<5U z%xrdzrcuo~GgcpR_4Dv74?%u6HmoK@sOC0r2As;L z2SN25{7nPKCp}ur1>I05ZP^3+jk2pkV{{hEIAAn_*zU*f^m;cl zVYrP&atZWC{b~P`AzSa%f-*k(qZVwU@D&8H2 z-Dr5*(03=&S5j^Mi~^*xM{u37CAU}3R>M=^S5?afb`dHRp~`po-mO_!CG zwaks^l%7W4{=RkHm;UpT-WI~dV{10FZ*KSoKCz8=eMC#oRMd}{c_rGd`V%?<3eDE* z*CQ97+;_|YgL7z(Y|MZFs0pY=O7NO1ENxu`Sbw4qO#VG^apYys4sJ4M%PJ5k;Wrtq zK@|f}4D(N~@B3{A_v;tJ6g@0;^)+J}YQ0R~Qu!6_yg+isY}BxH>z>6lkd=*Ijm_eY z*vL@9w%>AJ*=tJIUViaO*OxDGs118!_@> z@>voE-Fa|m6S)SM4E#gcg_ z)Hr0Oi$zzpxs>ACg0B)wTWqde1Wk$VSLD8`dn2#GNb467+6%C#eu8fPVW!Ry%A_ixX1z{V~lF-^8&u&cwWM1XbgDi_m<v*MG z`wYj-su)PTr&YX5+;vOu{V5nMEaUD7g|2oTi-80|LZYJAz2o*bd>8MgY%G8M{=Prs zvCH{QhJxsgKU@9*`<^Yg4u?m0H}rVJ!=UdyEN9QzoRJo-iwEwJ%_}{o)yj!n#$N5h ztTT0*bG6KUE?et=>Dv+^rcLy4@|opI-9`k(@bnmO$_%OQ#uJ=AN^B!F40qJ>tE@eu zUYnF}nng)}Q@`Nt_GDIlP>-Pd+Gfm|M_ZP_4LS^jnK|q|W6%ct?vUWN#IE&r`2o-O zhO}C=HuiKdK;;L-#-3d2EBa9cEu$sBc_z+zH*sXEVi&#I_@=&SWQ!(DCOPu`+o?Uy zY#{Xz3OSj(BQ+n=)3X^_$mnajZ4b%1EF#~UOE}#)@5hj}ZWT~~K$yj{T*-5P{w1!N znT@%VRbBKVFVm;)wIfwn5be2tN;S2v0<7e|gnq-aE%x|?#qlW&>~r+TG$c4GE~L?O z>tSh}BO;#6LpR-6ztT>!HsVVJJ4DvG$uy9-OZt*)9E&g!EYdBmM)G-OG!HzfAhq73 z=GJ)}y^8Q=QjZe4lWz}1a4Io08-5MF_F z%|Bwnupn|667yF-c4Z~iJJKj2zDU^!uDSAFW9(LEezgt4j2(t#`n}lXiJv}>8av!$ z3dkr&c$WUq0L`yj!(xBC9GSAgPL~}$nH=~xCYj@Cnsdx&x}!#J`4pwwrf+!!|0Zlz zel!ai-jEl4K^!~DZnzaJpKmZ!zc2lyTG=>3IjL%z6zMR#TG#&k?o7Juni-PTt7rSw z$PIdq$QqCGyp4&{nSh1QmhSk5L(o#4d&`D|DeUx{sN4C8N1jmtmt>1Ss-ceay8UKe zzkW0F<@2Rw+`*4ukf}Krn8#|LP z)6*UQnWhYs}lwI zg-+tb@l8H`hiUW5P}`ob=%)p79N3xYC9kxNHc1Yxq(Y=KSMl7^ea3ZZ-?B*m6`b_^ znb_qw@%rh9o&kIgIEJ4l=HsFJ#o=1ZMW}wh2T}2vGw!*<9?cS7i}*7dhdG%1>+W6; zQ~GSxitjC?`t|nu{HENo{jZQ^E^_CRW<4cLFeDn`g1c>OuBy3qn2j=?fFa@)#JZes zn?6YwbIBeYmk2!V`T&zh7E)sLs2$@UYc12LHF@yWI)(6j3VmdZlq?wH<(~a9cdo4S zx!uL8$My%;h}phaowz?8W-7HvmpV9YX~R55+$q`jPC-=N_PC^Rys|FVK|K~0$kyi4 zen9JJnsQY1I-vd&bmcr#uX#BDE@GU9JjuYxY3wfIc}YkupQyR(!Z1TgPP5VQo++8!;?bsfJZ()~RV{_nRoxdh1U-~vB_e`7C7iTWlBDln+(i!^6MJG}3f3(_sCV=Yemx8%WOe6f@S1ULhgbd5up)yYzi> z&u5V|?siTta8dVUiP7;m@cv#@iE*6&1}CEkON-wu>r;cEu185H>JQsxY2znq#?z3q zC!fAUvApUC$Ousu<{qdBelxMvKLOI*`?%63jsngvgH#{}!NL|ZyOTb@Cdb1bZpsP@ zKR!I=VwW*%gI(Qrwi%#lSe<&Rq9?^@k$Ll?+wAN>?|S_aV-|$3=*sP`x#nA#hctoX z*rTl-^bf49uaU+WB<2lsQd)L^)rt#^k-sNpqwG%{uqbvME6Yhax;>-^st?t9Wpo7>+bF>mvwj8XigMul;2ktRv zFYT?HxcDveW%R0z9IU7A?ad?mjFhfyWF$u%<^lOs?Fp(+ouUM!l4dy zPR<@cgh)t!wXYxvK2=gGh2$v_`p0)AyW?rq!0uzU%aiCF7&zK6`k7Te;EWeofj6wTd*L6MQWIT;wl}#oG#BPa9DH4}m7$A1B-I@au3eYx~)o%RvrP25;NYGB}`h zYj$eKFM>A)j;9y>e_hCfSl6&P_ z;ywbi?@bhkUmWQ9EuWPXo8zaz(hX(_8M46X_|SRzUqsJ)vRAf{1r45g{YxZ&T6S^7 zr{q*gffY_0%3q+9Rx?sb6mS^BFNn2Y+PX-$D;;K^*wU+CS%-1rJ3F@NPP4&c{a9d- z4`?zHlI;v7S-66WU8z#;kaMm7Fj$R?T>15s`J&pZK?8qP!CVMxZnqQK_IsC49?gkCcdIQRdf9A#n zry(JkwQ5a7R}dancTA30u8QubUHQ@f)YVO1&65Mv$x0QUWE(bxT)*xgAS4ZhtK2^P zw96&n&Qu6|Z6UX98+U{a%0sxqWay8~idQAz0QdH4xk8e}I4%OI=qb;W3)gWE>EcDRhQ=d#d0u!xYoqT>%@Z#F&dANIUmYS%)TH!%xi<7*VY$KZYINkB6W|8Cm6NqiT%DS*KD+%=#e3(kusQ7S)=E^wo-WU+&h6G z!;`Voqh@ENWR#Ol?n`{Ht6i0CZhd6QB<{i6?J0^P$`~;|@ql8jltlU=UY2`(q zYcTNsXzDjZje$7s!7WGg*OH$UZ(AqK&hc394pZE`lh$yjU~XGD?wD@~8UAx9ZO9Q_ zfQYCfT*Y{`m}mvYp)Uiq2&xc+-lBdGFgz^GPt`nOZHHH0{&J-kxJ^&LBk!FMW=4BH zlvzJ&Cg8nCj7vp~y#mY}14*1QVyBPQ2D>9J^KloUG6Tz%AGh-7I6SKhZ zulTjkw8P!MB&&M(qhp!T*$Eh)(nhG9n?TtcTxd8qDdKriUb}SQ?L(fX4>?Gp$ts(A znP|H?q1^2Agv?-msR#7;{ooW$wZB@yKg_(y9q-6KFrPK4|Y0(5lgaODwRF zwL8z!Z;)kSezI|7z0q@cwt0cu9^L27-}Q1g`lvjMS~W|vw;%52U1dwM31)?3=Y zfKnPxo>gSE&-^*46!^e`(GiVX-xX*|p?j&d8w4kYO z9tXctraW=kmuk{yRR2ApgTXgD3;I*O1kmqMsHRFKmYWVIB|P9^os`wEG)&BYmhy<2 zQe0^=vCB8eUK8QhZD*4R_9regT4h0hsNC?TP5Q3&i7;rJ#LkX&0U$8Y`bdd}@$B-W#D8U3HYW{criP>os1n@- zWWl0OV0)DmSYwa$)yb?z*LOCC#Wv0^srgc-CH#Xz$-ULL4Nhj?a$pV!4t^hrkjj5$ z!*kOBPay3-1BcJ=W0Mb3-Sdr+3s@cRZ)eSvp`9I5rFzU^iFr1UJ364t1U0uS)8^31 z0=Hi&-}(~5L!sG}x1vPoUxd%&0D*63%OV=y8M)j9JXgW?wHLh!zm z+sE-Iuf--_LrWiX%Dx-q9^Lh8xN#`^rqriaW1Hxmp*&QTKt9z#wwxZeYX;lGI*a9w zQc;NpaI(7^sRm4U4S}*(dA<|3DF8@LXAsDf+zWI?C1d-U=!&mPmj{E7x*S{0mD;$cbZ>XZIp6|+$9(quQJ8e3jB{u- z5cmc06oR*{w*H9zMe^)@D?uDMEjvEPxGbR3Jp1-^P@b-666gKw*qR!O)Kd-J{{kLct2yodhgK27 zU$x%)zvPMgE49+M*E*GSLw6~@h!q!_2Nt!AzFKOy3#e_BAWchr$aU>k=f|UcSHFyA zFI_GmwVJvcP|BB=Z_?p>jik=zE80I_4K=TpA;xD|t?RRs^p1N5W%=7H^$NoYdqTH@ zk)h|Cn%_qC1%O@<+h6R~xs}l9&Mx_KwAj9{Kg68dzIoI|h8+?VE3Fsf<^3vE*G;EX z4g15EF=wc6Gp1Q}y!^xg;e%RQ2&#{bYH#c_knFV9h5LM)Ju$Mz+#d##)(@R!Ey)>! zsn_!z%=@Onq98#;X@SZ)p)+eDKvX&b68Hs8c+JKjmchSP7Eo&Wdf-8viJ7*R+3TrM z+XgIBTwo}(U0fOr&i+gVp(Xj%{2A|t-CSWXb(|UsD7colc2)TS+($2$GgeHpn?LTK z4Qj$^qG!-BLssnlLF~%MN?O^GI}xCmyheoFuoh!nefH9wTOj-2jbCP0^yIU&GqcGl zK~s)&oyyl}xjoL6SA#`)BYHIpWgCNh>124fW($$R> zIN{XobC1kn^R&2_^51`GN&Z~&<>^ANJ#~DDv&;&b&N#g-6eRRr0u;I!J6g+?aFbt! z#R{OK{`LZB6&E&_D;(G7iZn*vcVUIUvMf*DY@e74#2OYyzM;4o15@aC^xPOwncG|3 zhq76+YiUCA@oJ&8@h_m8;Es!&xvbXT&hDNOylw_%%S5`>K(b&$K=XZQu;le0ZL$xc za=V&OrC*;*l=a2MJx|tL^BaRsycDO4_x}`cfMcyL`BV4hA#SbI6KK7enUOP;tTDnO zmTb%garaXV)7YM@@L^pm{*_MQ$aXi^@W;tbW;To-{F_Z1k9FWkBX9ve5IqIq-Q}$Xk>PIc$qN$4{bNJ=iM^v9vIm;suOmfzrSSH zMJ*~crneIi4Pv^;LE?TA*VAtnnbZTFIzTPij()823qYSrcff~8$MKue^oj*03Ea^D z;(W;?Gv#0LY^4Z|*pOl=C$!hptiPH4dW|H!%Sw=V_rptl%~lv*WW><$-Jn)pv5JV$ z><+b~p<{ICOR)4|0!8G?YwZSTk9ga|yNvdXOw3Opxk8B;QBiL3;=;=EAXOcJ!3|EH zHa?};D@HiH6`R?4eOmT}{y#ycJIV`H;JA4pcuq&~T_<}fe6yQVTQY#Za6U-*{FuMCtI-UBiqbgad!gGMSm@~-7Sa7=h35~GvYn6TR)Q@2 zEL<|-^WLa)eqGjmNrFpPS*^`Q)&J;IvhTNIK|7nEP0C}q+&-6IGg!_*ZLg$YCcWY7 z=rkKQygK~w$NW&2eEw>3Ra+Ztw1>#&b+0Xc`fm3#&x#=_u>c=@SW;4eqYX6EsHXh! zr#Z%k9a?7ByAU0}A8DXoUA8_9rUd}G4BxsX;M(z_vGd!}N))2p31cU!mJ=eANLjJ{ z9m--CPoD_CUv?I!yi^54C?K}RAE zgvbukUV`m%)3kc-H(f9x3aBkWc$6t&YZ0~ zuO7QN(z=0}J4;PeAfn{d$*z-pxW4K_AE;FDSevzd>l*U2iwbw~7$>f88ZClS{S7Le z_04wN7$DPEYc`sYSh0tsJ#doMnUatbJi?%LySqC&DI%-&mZ}TW&FA}AwJ{G1xNN*Q z6ndXIw)iepZhv0vi#+}@c{Ah=vew59)&iTLcs78wt+AT3d#o*ogH9`m}4J>1!%%%A~A*E zW>=8;3$&wrFW$Z0QobpUC6-I;O-2SgcE*BN&VzZ~L2!jA7n3&hd#Ahlj8qi>LC?s2 zdG=13zEa!X<+6d_Yk8vJCJ<~}$F^AATHn0=8Z9%v>u(Wel0s|c0E0QG^C5Ftm$-T@ z4_h*f_RP@ZFTh8c?WlDiB!z9?2j&7vA^Lua>=CD{bh4s@R83`tTLfAxYXfiZ&|VYf z376r0#7qHHV>f}`FbwPxi4@)aK-qT`l{zv29&@JI|6ynqK^*97;;0GDg`!%Nz${~v;yIZ=U6$wB* zRvNvcpH@c6#l&gM=DzXrnN!TvL+NEF8z&o%kCn{Cm9hYbO-*1J{0Qxr^|9P&swE3F zDd+x`_6Pg0o1uRn8%T}401ere@P z`lcFCc{n-whp8%IPBT%I z)R-7vao4_pxggDp{ACzamUU%_l!RnO4kBy&e}XQV`=&-6dB&(FsJd&sXd^GLNUAHU z@W~`|uZcAkM^tN@=pCkA?<CMaxKhoEOGex-ZEwi2lZOo`;eIsj$%@ZP$;;UmWp}eo7WxH z!O|}>1yTx3NB-Polqv2%x1vTnZ!$xY#Q#A*_GjHc<)Pwb1A>F|O+;Z%r>^tUfs)Gg zpnsQ0vKZ-R2YrCBi4q{TTn9uCVOM1T^R1zPFELlw!K_g#YUm;z_7|o3M_u=_a4M7l z#aN@Q1QxuBXIoz8s2`E$r4Xbn zVyfDid=p744+g-j7hKi$yZ6)Lic*pcQ3=Y)DaaNvagKAb3^hyJmBARnZPjV)O4Dcm zZ;)(zeAUL`;YOw-;L*IhkHK|KP9`I5myp)@^*gc8&uGPctg9P_uC^PqqW$&`b{F2@ zuK2zBr2N>1NB>&y)TsEMR~pNG0D(4q^QuzX-Q3PPbmxDHG7{Q%pf7mXhn%V@G^==| zRhZZnc{rGDWbN6fzT7D@Gc0P%nyxVt50x4|lu*rsx+v5m_8Ul%u#h@pG$5&xF9{GK zKVLP3-GU5W=Q#B|Q|erw4H|@h73i4y!t0>q7(Da}e0C?qJ@wNA)l^&Q(Wy(CjUTrn@xx3Y_F-a7!pycoa0(DETj6+Wo5A_24v_e-RGR8c2R;cXeMAO9P=Y*1X3xrnm27H5LT*K&&%yR{=u zkJkL>v>@nzPy?rULgE8}8ztQe*&#e7xuKOT8}bWWKre>U$>5^~#Yl0hzH5FWFn|TJ zDvovq!6aan5mLREQ?G=e5=ck2t#5d2{>8W;Oa8ZR#Qq19wNDG&C0ADvfXwuX?4Q&B zhhnpT5CNj+a*Coq!g)=i&jsO+$wy^RLYwtkVHDzIOQ^XcUdW-0&wB6-_N{o*em{VS z!?nfW0?~`5^pwLF>YbdEKegv}El6546xt%dH)WTLZE@fD702N04IVTJ3H@50_B|^l zQ_Ok+m+Y)MQX0XZM2t%@2T0N5xA%sevqOK6!i`+#M9!Atw0mDhq4?d-hx`0nu?P2) zZ=C%+3}tZg?X2afX?_DcORQ^(Mt6>mRiab#`SH4d&gMLjMaU<|u!4eTn;sk$J{Fd= zlSu7Rg2*27)EW!!qI&F7Z{rt8S29|5HsXK=*IC@FX-P*)nm7AvntAY3Tz&Vh+pDwH zZgU&8C)G44XTy`KeGI=O60C-9crfQLxGFjRP*$)y zPP-d&d>9SEbrjSFA4%@+OLgPgLM=D31r(Yu(7}0k0@)~J)PR!=$0wjG*@djNMd1b~ z58yJ$j+)$?+g>VOsn8z$zDf5Ge>>TED3twH;HLkHx{6nz99TP4)}4w7r9GkE$4EM- zD-(y2DRBey>g~>Tx8}9OY}O`WNyL=2i~W+2RRRH-y&ea&Vz;&$_gq^z$(p>dm&JIPrjPuO#F1UD8UCP@SnY)3rPeKBB<(&p= zS4ZKuwnPb8Y9X5%0cY`=o8p0dV?ZV>LTMg1&6Abg{hn1}F(sSn?-IH?Ix~k;0oOWFDhvmj_-Ahwr+6|CS5l($ zeqQr>|0QrQSZFszvXuC9^g!#&=OKNAZ)ZRZuIzKbIoi}{GsObap!qGQ>2A2d`m>Bc z(>-E3&w+5B#``H3ZFLIq&_urPY339l>H0URb-x8ahAtFjE_!}eMW0wYc~h!>o^6ue zKk5H!ELyWwdVAOrd+Oo#`JptKK?gZ->!)nZR`^t%SQx!xvZA}00sHyv z6v!I`7iaL z0!9**85~*ZnWzNW%!oAqM7c(|ku7}x%Jju`qJ_7dOY`1xYfe7db{p#8kpFU}EzM-n zC_9Y~q#B5;55)>D^}Q|-Csg)^4qzL7RhyIDPf=N}=nNkuuko>}v3EN>So&~eOFzq# zMqz%GzeS*2Dgv5MVI8M(e(y*eGd=WXg>-w1@IvPC$zG_sGwHTIbU*y3Y=|lmyh2># zKxZB~;Q9`69b;aLb+Ng2*q3?z}j4kubJ@y0Z;nN53djDTXJqgatw6WNE*T^NSH@0$tzsab!NA z$ymSHDEKl;^ZS|BWE^D*x4iE;%FQ*ijp%N_E*ps6eb)wF5BnYXG{oPW;-v8LD{qDX z*GfxQ)I*yiGIlc~#kZboF$ic65)vv0Pfb>fmE1#$3z;Po{M5#tE;Pylxi3pD(}(@n zX$_Dq{#fkWU%0SH?os~(*+2hZVgA2u>|*iuWUuP{EzkM$5zTxjXsfceG2oJ9v8tZa z%c~jpaGYnX36Iu}o9s^5fj|NGG)?Arj})x8KF^PVO|F1~8GKU2xe0@=zzUx6mJu=` z_3!u234tIWRyF%Vv$*7@iIQ%+L-M+7FryWW(Mi-7KU3cV?{n+);|AYmrC@2$_lYmw z@hc51ajF(Qpn->ssBah?apqyMNeAk9HGc&W&87veEB^^I|bpZF-_&Ts#8Xr14 z+l%d>aA6AEA{&KMi!8V!&!0=<9R<%E1xiW-PZ;BwoVRYDli|{|qCLl(+w|C}tMQeD z+MqH-zr)+{H+G#J?08{$VF>T0PQvP@t{IKYp=S4y$0!IKMf;s*j$+)SzPA4CMp7e$ zE38M5%5xihY#cxc08&dGPbwiwQ@y7YM2*^J?4D7ObuPg4(>EqVvl#bQAHU|xc|U^J z>_8ZEnn=_aJ1yAgx6Qrd$nGso?G24)hB$rOsp}=2dueq9tL~srRdZWp%0t^owcE>F ztIjfDDk}1bZBKsO!uYzD(!BP3J#BlLOBGkUPquq@t^Qc|a92)LHnl~badL~MQFDf* z?qX9-upR9(9O&IX+*60B6NNLb5&G&f;mc$qXxwwL$R8W0F{-lm<&{qgR@$Gn zy}_F`TOqiVF1YkAjk9vl6k#HZGNzsoRR+5sUd~rJ^LV@NK4W;h6%fqA0nKR?k@O0Z zW<6(tzw(YF)s8FdX3C%Zq!+8_CQQt?qI2IzIU(Fn(t^_&@bq7OvH~eVrPE@X zMVThe5n@rvKh~hTS6gDeRQuy4((Ab}g=eF}RMUsgy~n3Cn8U)p9qLBS6-i3N6j45a z&Lx1f?7`Oc4uIErmXj`5usSXVEbAzd*zJ0d}u?z@b)+KW?*(nI1+Ht z{0hH%Eu8Gv!AV>1j?+qZFq-`9PLJJ!TvsVr_$=d~_o=1Pf)TPw_5db)IC70S8@Sj> z)J<5|Jcg|&57plYWR5=r0|s?6rflo5UfvC8vK_j8B_ii;Kxh2M;ZyA%PNln1&{Y%X z9*M)xmJdL~dt*1Z#GU&0B(Z>smb?L|Xi2PUgg4f!FamK&Qn|NTQ%qXuizhl&W=EEp zsIPL|*%4Zb6^FdLgD2$d1mbN^4&{`}pJtoU9`P6C@0}MfW{okiOmE=sA!9n%&tV*cmb>Z!?cuQlR1xMut00MQ1c7P z@-HAS;BjAjmMk2dANcRVb3tHab-yOi^JNeWdvm2A66rZ05rTPC z@L5O(8o-s0en?%r&@^{?d=4-dK$}7O-(>gyeH-xq^nQ^r#J-0-f8j3I+8TS{Ef`bX z=&CQ>@0_LkzCv6&k;&<2Lx0UuUKwB^;oiRa;U!rM=wN zx@9HjO8%iRdjHE7_UHMoD10&g>e?$M&zdN-fcj$QrXq9U`s%Knt_@mNUJ3u0GV|G_QWikk8Vn|w0Y!u?9=?x>iGATTdAc*k9o}n6 z^Rj~(a@Gxa$$hrR0$)1DWBUM%Xx0L&*r!@nWjZW77FKb+fZ z^?Lk?m~(#M3&aC@!2 z@{G;bN5&_S!SyC2m<_Nc0}N9$I`qYeySBvHkY!h=L46hTb8a|5&+YugKMiEyMQCw> z*%zq+Z+XyV_QqE!X}@3yn_3qt?gtJfV~1%x_bPi|fmzjN9&t>36P*f$@@!qpk(OZ- zH2MjN7AuCVdkYR6`dX$%$lx< zJ+pG(vpA!JBGmTngv@h_msTu#fQyu3dcTmcY>R<22~z%8_S| zkQxKR`O!mK+I@;SFcp8a&1AGO;ij|mh2#?pc4~9gfCR>Uf=ADZXH2?15`Ow*iCfucK=fB}{ zL^rfOPZQeX^sW?z?1SAxhW3m=ywxS)_r>xFRcIvuDDIFSKMhm^>lH_aChQis<+^B< zqu!mfUw#jqPVzt4NuvKtx5^PXCW~^4m;TPRd9(0qEd1iAl3hH(t@eWQtVGCS z03Dx1>ID-*YX3GM3jy748W0@7{L}kC%;DP~Zw{){g|QL10KbnYaSnd%)54zmgrjLM zImpi;4VKq|Ex-QL{9mjpz6tKUH_o$NpQa*9oZ4NPIz7FxUH`!Wq`V(y7~!mGlA5oP zz-j!T_q|AQ=h*ksFY+?0R8!qe_8#hus{3<*Ei;zy`NRIfk zPct;G(MOFWfFB#P5glaL*&o$0lig0UcG4LzCBdvL^V_5c3($8sgP+T_Ja9!`!J4Z3 zCxZm`OY5$Yv$J30P%iLVr{~X8P(_<7T>Vl+4sFM&Vp$c5gyB^fLmt&70(A-(uvu z4kJhaw^3kg5}MD`ExN460n?%#G;;7)9_)IfC&N(jSVe?MVWAIvt37V~YAC>I{7Y#6 z_AjBGDER>j4Sgjc*7(QAcxG+kiaB?~0di$or;c&dX|qzJDjxbG{@Yi<{HS~S&Ev1r zD-Q~BTYl&$fkxEOi@3eGAJEWP4`!PrOQrPGe{^VY)@#=%hx4{;-*hVtLLJaRr`>YB9d%QUp0f}|B`&Anr`{!c_s3GA6I>| zVc$Xy8^uEr4_dWmlpXu7X*!I?B11G|rbRvLfgPT727>08=AvX{_z=Vt81Eh`V>J;2u%#9T7%f98wd-_5Nxa;(`tX__XI%ni95 zUA3A~P0xR&EjpG=dR&(>o131*enmj*)^S@+%U}%!sCT3< z{OK}2$j?2oQ5=nz^OK>@b9CdJ+zVO-i+nXlB<{mv5gU7__ z>lJU92s8oCKn`_Ael<_^pWUoK;$6F3t-q#AXIvh-qFZKiVU5vvU$yqkDB{#KJ~LjvJ*s>g>-awKOGciN?JZ+l$u>Y@q32Id zslHEF<)-Eui`~ET;<%olLnw~jY^kmByz4?Ayl$--0l)luL|13WUPf%Hc0-m+55wR< z4F1eCB;sHp`HtKDE%W++vsk6zeg3O2KR>Nem2BAXDfV8UP1kQWn`!0Q+LGG!nK)%( ztO$((@8vI6cfL%fe%i~KXnJC*B%jkHRa-BPmT*{~@G_B~x4iP2alauHTcwkF&CsE)9eiExuPtm_dAC1A*3W3Sf@Qa6bw{BA;sPjdCTPS8 z;KBRws4{s4ku!QrXJK0rH*G??Un}dSaB8He*j1yCqZf(j85_SIOXF?3mSeS6Ia{nA z((I-A*(MJt1OFQ=o~N%}yH(<{aJ)}XOkM+&$EV-fz1TW`kL>Eak`Qwm3fuDs%pmy= zkY;v+K(I_-uM=DLK`PnPhuSs@lA!-;mZ}W)KP1y|-EB2_**$M+U6Qd-QG}M#a0)1` z813dD@p7zBH{Kr~L7w-)E(3b0G0#3`ZQb3acIXez>euB>tl6r3rf&07NL^zFTQ+gJ zDLqV2A2n(7-L|k1_V21yWiijB?4$q4)&Dd_Rs3dtLv+(S8{4%^-Z<1PkOUyWukZQrhQyhb{HjV61U@++3{<_*D{p@-Lp zF^dD@aZPhr|GnOVwm97Be(xz|!qitd^=n_Q z+~{svr2#Uy@;^uA;Y)JFPrAJR>uo|dLz>Z;Nt}vN`V1Gi{4QKk|0hymu1Xdb; zU7DE4ZtH3}(0z33MlDY69q9Zejn;@VysDda-(iBGeg2_*6(8*2tD{QjTysHOE%DT` zVG(}|x#mHY;qdq`MM<2q%)RA$GdtmlO{D$)t&O?3zwV~xHMZ0Q+<*@3O`vu6jG=4o zKa*-A%!PvYC4sN4knzu5I99zT*7SsrP2kI(FZ~|wZ=Kc?AA&SB{C+PwN3-7YDD?Fe zkO-PY8kLG(qtF8bugdXBLEo*M|1kB(j7|ND!f>c!DdG@eYUviOqY4Y$o_hfXw+8QH zfTy-%TFn8-3C z&-1Lc?sea5!F#~fwx4ln7Mq{60V-8M|1O)FG+8lxMUf+W_(UyjJ8bjAOCLTh73;<`VkcDE8~7cvmA=v9C>8_48?|BzEd>VkzE?L|oTrRi1O3{?MH zGi}$jhu_q%jXTfjuEs2%&IJdOC@A!U#o|73kk0Y9o3>ZzlUFeA9+>$7IA%Yz0phHJXBmaZw1yINVUdz34v zHkU*$R~3;SVZY3xfsJoH55L_O+7nv zyek`QoKc{qU|x&)aPYC*;?+ZC5_>DwCB2`HZ^;sV4zF?W=yl(AasO-ibWFd&@IkW2 zM*8J=uo}&*BMuj##>U9IwLbz0$#EWgb`${xrgH29!?_(j?a$r*L?7BP!S>r}?S(;8 z$2Ey+po^_1o-`mPVbk=?cm_r)dK{z7qN4gva|V1SpqfkF^NFjNj`ZH_%N22&mue!j zeYl2MyP_5qogv{@R+5<^OQ!MM)wPYqXTWoeBtAWPy2+C|079L+;DZWdYVDTrQ^OlD z*k~ELwT60IjZDoq|G;vgsMa?-=v9v=MWnDGV@3G+QS7z;WIn!2>JrXAwg zPui0rJph9)mB-oelwQd{J9Zc_%~P#RY4&5jkC%a9p4W)K!*MVv3VL-Z)#LV~(2g)C5=(@Y%$UW+XBPg|^Y+~r^``*n7 zZbwUjliZKgIdv#ojm~#s*Z6w14`v$!V+-G+eyu8(ST( z$$thN&#SsyWM9;Cw|9K_mV%L4=H$l$gfjutt2}3q=NwTWh17xbG3T0ToMau*=1Hd= zz3m5}DW^fXJJP&3du*{i??6kUb+zwP_?4}ef{Ik~c+0$~ks!KEDaa|NGLxH%lDGMw zl;Yq$x0@m zS#hW7Gvn_V@mSuKGsX74)6(znP8)j2CMfyf;V#1^L*tU7Yspe2(R9#;#{9e)0(Eg4 zw9|m^yR=bun0{6$e|e2vtNMN>*!3_gCu!#z!$SmitCJuhhgt==-1$9zlj#jJpuV1d*zdH#g0E*({(tJl}hG@*PCoh2dcv z<;z!H-F4WM#0ij`hMP!lKVf zS$}Ue@{_T$Wc!7ltMT)UghS%i09H909<_i92xTi!Zel}imxjs@^3 z_8KH0ts0hQhG%u4W`fAgPMn`+a8@Id^Fc|(aX6Fgw%5z2jL4;VMx3h)lWAYjjbDJ6 zy)n{0wMSB-Vlvgxd>PZSL^1fP+uzLyF3-t#jQfY_B^D7vt$2vg(9pHxSDDw*`s@*9 zYP)J>f0T7UnR)%%SN$B~Qo?32>oyN?{%}b}L z7a9Wp9NG>okM&ehQRxe7^PZQt9d?$?+oQoZ-`%Na5q^me>g?r9?Mq3QQAOw}boP6{ z2BoC%x@c#zbL-~Sdg(oqQ6+hB<6^N51{Q1^))KD&fn7K?-s)8C||e;YRqK?XF%3s2F!y{1y-;1e>Oe17nWlFR;jzS<@h5 zV}H<;6CWAPdiC#EP1OZlRE1z=!?F=W8K$qb>+&{7zAo{7g@A;%Sd~y`X?1_Ri}~~m zlq1Mi?S)6HP9?Ax`->(6R`h)w=`ldzAHOI7KbVCjd*RO|$Ib6rAiJ?tsTj}ke2QD3qs63K7{Y0P z<{N*C*^s;=r~>YnJ|D=M&D3$3vBy?bG~@5_$zSm^Q}j$}V*Q2dm2vDtv$qAp;7q8V z>J}`KWwrm7Z;TUC*<(D4}rnd1dCj+1}({ z@5G<;D-X0NOZLy-moWHBqhwV7*w{P4(`GTBtjFWr7mlK~4Y;A6U z0Jjv-@7Oy!Ic(M5$0%FAinGifo?K}9h<>Bt)RSnaAFq>Do|ff%8s7;;nrFxrXLJ;61!rd8XjxXWY`?K;%JWuok^?E^Z%5mn zNM!n)8w1t09v)#JHMQF$wcB}Buea2Sp>m^B{po67zj{U_g}Si1R_sxb3n`X$=0_hDj5Xp<+YQ;Dl`zp@WBgB*NIp)QTc64pQQml-v@) z&%%-tHkm0$eTwA|uAJ8SInr@OuhGnqynkFFc8@SY53Sw@ERlUGAkb^A6fH~9 zr{W>`R|8+2Ls$$Rr(bD_y--*}CK{=ecdE=a_nVi_RX3JSpHGcBDZaZ8_;CrYzegrk zO2ueZN4-HNGl&LP1Fp*#U!7p0W83Dx-Ns9Y*)BK$c@l(YaEzbe@{u)MG+& ztlnv(1y6dICFjtii$Os}R7whsqW19kq1A!rlrG6cHAzM@m~?YgC|9=&_7X+Sz;H?( zh4Szz=TMuN+`m}kV^6p^mH1#`@)T`Q6h(?>c4Xw|WnP9Xu7?(qUb5eYxq zK$eG0(Zq%WP6^f*5Kw2zm9LYj1;S6O)uP6Ia{ zq(MUET_VaGX6Zm}3@oAD9zDKp@{X3Vr`}c>12b0Q##W&ds|gI%PLGEh#>cpo-5$}p zIRz&UepN3SNdPFY=_9GiZnU(z*#je{vo`8)B$SqAWvPG!GvGOG@PukIO2oaNsZ-fI ziaYdEht;SzWd9yIG0JD#weN4(TL;`I@?7yi^^aky2)Ar!GvCPDK`cBXofET9Hjp8} z;vzDbscrssmSl{Fc$H2vnKRV0q%q47oUzP?4PfR9SAXh&YkEBgjSb~``vIF48p?I{ zuMa`^Km4ns)SKV^-#r-8@7^m&5Q_%a!E45`gJ?wREunKL;}lNDt7>D^QR)rR(a^-zV3PkRC)JSs}hTo3F?uz}z`eq=5KA$roECPR77X z8?B`&o_UJv9D}oV=qRhX|8Kv67>}^{KDa1$q+vHfks?H{2k`yPUreA=o`W{Fqh zXq1+-Iu(!PD zH2^x-4F`6VLwHrJBA|MM6@8()EWjQe0XM20xYc^FoL1$O`Gx-MVbHa!=Xtq8C zcfqF3tMd@fNqnh%c2q`%RJ}kZB|mKYNYYj=e$PTIL1THpg6hhkCHWXC&;aQ==rU8` zx01fbSmqjBgwqVR2wXb9+61%dx4;DOWzCp0#Z4pbH11a{c2#_NRk}VM{;`1nuo@9BU0XSRDKFRbb*u~L8JSf1eWJ4S^cq;}nZUg$?Q{^6yYymn^v>PI z))4JQA`o-nW$?-nOAww(;GKX?%)=qVI36?FYy36`?7~jv0E<@Q&2Ld*Y7`foTkMuu$y;^&y)TZ5FcxBPG!h(a8t?>GI3b;T`>2l{X z48CpqQ`58{XW66DzWU`PmC5%j`hXK};p;lugT}}_1^R~!=SS;F z(XKIL`uJAJ>&Gv?DtcpDka#nhzyS8du81vsxVru%cm3MZ7yI?lzEuSx5Kd9&`V{M{ z*(U-Lds6Mph6^f3-1#U-Lqq5tzP}wvdivwrwbvxNn~MIIhe?B5&^?g1gDdHa@`whf z^`g{>2ZxOtCjllL`Es8k`ddc;#Urj!YlV%hX$GG&>t47&}YpBXBy@8NzzddB7Mx zqzc`-u$!tIu1pEMYjUwmE9X0+`~9;TWcmESIt%2?(alL2KzgAx+?XELziu*no?-do zaQbH4U%-&!vUjiCHrh#6%5CM&W>$7QW6}aO=KMMIc$L$BorZKZwM!r;g+6kcFs`pv z-)Y4hFay4+f}MiGXFsRuoUI^t|i4K*vj?n2^iaJ<5sWMVIp=ux*snMdES? z*Jo@(iO2MMTsihz7fiGqE3ouc_hE$G!$-2Is*vZGqHOf?_hg&ss<#@et}~cy0^OFT z!~VvbEW&AVgQjfhhfJxk=fZTXS%GPE*Rh9dtJuoEWif5?c*ZNE?m#t$z9_GjN~WE3 z{w%Nit)F7PR&JeFU1QDYlEV)QUb1yHy{J$%-zjcDD{d?=C-b}*yq?|Z@%fD1ycoE? zXt0tCXVfr&Eo5Tkv3m6jxGIHcpW~bVN@Pf)8ZLs}W#14JkUS#=neE-F*Q{@ga zuciFCTdLlQVCAh}^<6)ZZK*5d#%c*%{$enj46o>z<3?HukjHGgIU8j=F+B~;X(#Uv z=mSD+RQUP#`3UW<(cCN#wFd5Cm-xzl!aahO<>C+46p6ga#zmwt`Mg{cpG|I;n-aO~ zDFOJqS%1zYEctxi8CxWKsX+{Sn}+hfs!JsUQI}vvK+lYSUiSzl50 zmwEI+9&35u$@__W_hRvNu+5UAmj5u-jAhy-fo^x|Y#a8^N%nVndR>8(zK8b=FR5Re zK_6AWyZ@}a`&IXN5%xv)awM^C${WRGR=0s%!Rm##us4CE0+WG_!@Pwh#`@G_nT$0n zA=|R}K}jUx5{_Vg{7(EALv%!bHK~CFK&))OrUp^6@1&^TuU4NNjn(UkF{)~Ef8YE7 zyL?sS>+AA_HL%Tv`?5>zPDJvQsR$Mg%>vio`Z1N8PAx})wZd()iNelQ zyx2)JrtEx55w9=snL?kB`XPrLYy)YV?-9~Tm>5C{CU2^+9Pe8aN?#s%nYCK%CkB`f z#x)g)#-$*=3}m%d>>hS9rcKyZxclE+fb^KovU|Q|Z?^$yRiIm5u>y`f4i^E^c+!SA@p$;mYQFLto8MO?@1hWYY7hx zwaW)GSMeng955S7h_fg7PJ1p3_>v$@jOrfq^3uu+8f*;}sEy?T$5N+Gfk5ep{@%Uj zj=9}Eu>;BwE2oaB^KhethzPI@5PKeW`FHP4h4tUArD_J~se&GtjH0U8wQFMV9)V!8 zuMSYG(A>EFSI`%2L`LYxcUuPta+c`EB+zG#`5C|QvttuOrOy3XL`;Ias?1Gzn+DJ6 z^s{w!!YaHi*v}e=GBa6_#y^mM`5wdp&yYPxBfpm(W^cStHFxRx2=uu_-Cuw6l)Vxd z8yushA{^PnJ5ZM+JZfe%Kex z5GbHZef>Ke4}OU0M_VSG`C?bbsAoLveTS}TvG9~OSyk%!HV&V)|XR@?BiLXHM2Jk=MbwPmN+8$43m9VaqiuZP-eF&0g@1G2m(q%3Sd4y zw+{h_bR+9%e<%|Gm}LGF`1=nO@c(ZIKv#-&GBCO9uIcS3PUGXKrJ>6r%!1iVbZ}7;x?A{w5qVkvYIqF#F zw50l+2Hk(#w_aK^D7g*nE@Ub&E0dS!pLnvwr~)W$2-CmMng9FzJq-i_FqHCzDdFdn z`(D@w*=ZsoL;pAr*v&ULkuYn>oO}nG1^^JI7*M|zumb=6>QD0Dg&1e)^EgQTw2cz) ztSNKhAn*__VDRC9$Ql*vtgNL%I~+WJ2tHZ(gF;w))dkC6Y*4GklaG)H?o3H%0dm@7i7MQ$m;D#IPXZxb<;P}E(`9}4O-7eO z&etzU$=6JOAy(z?aAq*fAEu{jm^GhA1O}8wEM9F;|N1cD?z4s7>^|?Sc@#WR?9sbQ z?N)&#!d)LqC#AwAEzvUlF2JxY7l>LeY5n2D@Kp@8>axGf-?CR7MNpzVaR>Or%eGGm zkCO##P_o>{h+oZCW{j~Zb6e>f<9Wa~Y>5;*ub;+%&zd+& zL9uT0E@uDJnt4xCUecBb#X5-BK;QEXE7cGqU!mFg5s6m?jksH->C}%*7Bgu}S?#>x zblf28unG!zHDsW8HNqut-aWr~X#iCM`fhC-T?B{&C(}%{H7nI&1qcU7+iH(T6QFhJxRLNI^m^eg!2}X)$cQU?rDbTNGuMg1aXP}Sc#}e*&^9e z)eae2hWJVl%b6MM?H>6`%bLpVaedaMY4(CI(G{3hr3E$=XCt2y{>HfMS7$O%(5g!4 zlRP%_@oHizl^V;^PV?idc|N^hAs5gh00JIL^@YAM#!OQWoYE^T_R09M+5I0HR2y43 zwZ`QYle|U=g{?l`vk>zxi{yM*-ZLTdH#S=!@h%t@F$A43n$uPsiRh@vWYiY1j^}vxB2T=v5}5*S@B5etsoUDf}nlf z3Y}fkhLz&Y2DnHvL3uO|wYa{no}TCNWx{l`@YfDyJaw=8ye9ExEvF-!N30PPG}}<; zKvg>2N`Hi7%4%Y?+gfZ#-{czAJO1D)r@!>lQXun%2Bp4UV2iHrhvfKm96@|#h)v}x z)|Z!ek8Vjcs|Ztk%FAlmDZG0~b&s3y`z?Pu0x)JXcJ+`tu|j81+#c#We)z$-+L&Bs zuK)kQ9Ly|o*!cA+FKjyjMUP@o%=W)4=y@Ku zeeNVTNuGO28yL9%+X}Q_u%Fo^HvEUzIKzNt$X#ah`pfxT7u9f?HlxY3=9`}qfe^Jd z%cH8xV0wL=b#P@>1Q9_A5*q3DChf5%m-hEI>s4PL@e5khA_trg5Y^Z-o3+k@XCh}?O~UG4iE>Oo-S?#M^* zZ4iat!WZO+LxR| zL7-tq4t0|Ixx65MyNwj~F3&&jyk)Z=)n8nKy>DgZcATG9#JMIXqNl zKU`SaOkUg_-54hYSy4aidZ{2jKe2s^17gKZ!Urw%cHaRp`#YqOFL-a(;g9My9xVnj z2QH_p+WR6j7`uLtuAdBy0+nQmDZs`xu02cpA8a{)pIq;|)2+=7P4g!UM6H$@Kh}t? zXtbpHR$)}IVG8r!8R?LS{=2)ky<&e2rMWtD@8BuGY57(ybhPS!lG2Z$&{vl#SdxMA zS1t=GK5pHu8Do^L^>5%{ciTqwm$go{ei9E#Lc zH?FuZ-I|X0W0Ki%em&So4Qg%uQnU79)wg4WhrVg)B1UO3ZFv9CP3<|5kwu2F;;zNW z!FKg~JC~AHfv#8^-_{Ew%w+ZVQSP7bsU-5@ms|E!=S6(1;?Z|G+0r%lxlR^FBpbn( zUh=ES(KQQqfngHf2nZo)VFFw|i?}$ENs3rIS~oF*`wmweOgOcm3i|YAi1|^O`rcWk z*$W-3xVGV943cuyr{)o;j*c3?U-Ti3r=wBcnJ2WbnwJe&YnQwDs+4F* zhPNaZh1pZHboqnoU)A3h0SpDBR~mMTw|gjoD^Wm$^sms#yzSa4t}O9j-!!Fw!7b3+ z#zVcm;h1?u`VoH+71-gyz|*Xd3HO5~b=b7>e? zD0rbG)gjc4(!!qG>&~l(?4Xwf`Pk8@x9a*kqGd+@zVkCD9e4#p+p2?2Ntup>lKtRA z=PG|I#g3WH%p)Fp3a}?v1a0`_QiF99cf4g-b@77v0ejS-$oo6`D>~#6^j41_;R#x9 ziH7C7+43bDt%gZ#^WxY1a)ojzg&h$bsUZUq?Bzxc)Nf&~LP6{$-+@jLZJ};DYad5S zk6Hr20w9oaqa;4B##l?5i>Yyo`%ZRNN5F>Etc1yL3ACZPLVVKIF@&z=z}tT?8PVLx zOH2jgHBJ7Jt!~_`E-U)Aqc36GOg!Mi%3GTFBWgV_Mj-0Fnn4M*U7@f?j45}w{@0jv zF|*GNS39sfF?-wf(~UodQoBTZDe&aL-dVH$6nzwr3C-L6ku=B#@I2jy%P(J_1wwL_ zNlKlB$6YJE*4aGstXkY#JX#;JVA#1l&4aD(DeBIbwwJHbhQanF{;1a}RKQTR?Dua{ zMx(4jq1}MJnxAPu344K#TM8FhBrQq2^)`EKr@Z^~^oRXpohg2^RI@7gEyj|LHwUOq zVz+}75l2%AUmYKh>6E%f>gEsi%he^gpaln&=gNZU}%QhD%M%s=-%N-L2>HwuTE&;CqBa7#gPd?48*;IGJ$9rFMlnVG|ngcNTK{4`ORXMhLl%?@J z+T`oeFdvQrhZ1SyJ6(2NLow+Fad7n-Oh?C}m6ble>euj37b-QLgCyGztv+J!f z&A4KhV0Gc`&bf5PnGz>gd;3#$$yL-wu8RwsMO7Hf^N3{ENh{t);JtJecu?^49ZvF0 zz_=KjEhWI>p8JTt4+HTU_Tui6@|mndkAH90fSj)zZ6E}XQ2YOUrC58fX% zAx0nm0Dzmlvy&?!ATCYO|5tGMPnu=O^8c7?{%7KO*$X0<|DGS!j(2j^i^ zc+9o}(i4XgH=))|+yoigmIah=um$Cf0maNO$uBtB#oqqw{1c2e`gg@8tSGVrj1>XO z*nj4-RU7zMoF=)AE=S=`f^QH=SxD4PuEx&P0&>H-U|bUeBIW)6?;m6eqCbhn9{Bi0 zj4bB{mDTwB)lEYK43V=@o5Q!-VS_1G<0kT`funGLTa)pxcADfLe@|4?*CiUM@hFo! z^8W}NcPaT#&W^`W835U2WBTSVqjgvF?m10Q`=YKNll2?k5%{gpWDG`8}wfs9$WVtbp9H6 z6*1owLxZ1uEpV$cEcFai<#wr<*Vc4#5k?y>z{IBm%W*a28a|wF?PH68YI7_p8kh8e z1nReQQ6e!*|GRAaEiIo!y^`>lxEso`wwbmg-K~}hZL|pGW4xb*d3kqQ?bfja37yk> zD*_=VZ>h0eLs*%Ms{Q8xBY`E`2k4ZM2>S4gD&QgB?}|mQ%nt6t@jM|Ep+$A3A>8EAN< z>PrX-9DybSP|I_mkb$&TY@U`fZ@wuSpYg(%*RzqE6A6V#VG2X-49uJwk#<#v7``hI zlbV`#>wCM(DV)|B2f^v(xQ8szi9PNEG{rR*^L#4s$`%(OaE7ksG#*JQ2dvPibM)$% zAZ2$ZzLz^DyDHF&G!HnJO3d*WBJF&nygY*mSYGmVP6yJx%F~*1s^4UWPm2;3sjk%l z{I6rKWIwEbcjY#_w(lbCm!@MA>#`ac$%J9zFg?v(bMGmc8D=$ZUbQ+ga$D-l#;pNn zNt>a;Strj|*1|nwr2#FxxBkeBR@|UbN#{i45TUK{P5Nd;vLi2HLp%A zUSV-BhzZ}fW0u!sd{wy26Kv9S`@{WYA_8J5f@|a!h_`G2anOiScYe@5{2Mm0%kCCv zln$g>seukJA0N)$Ly9*Mzw>IO!kOY5NN7$$lpwc3rDX)3wM|qvptmGsAQQK8`sEk9 zM!)gqA5+vt2%Bqe+z&UPU)%gaLtRn5hli$BPA~jq2&cKOtaAZ6+49d^FWBt*(z9kl z&I=3_J#7N&RouXHj{xXs0E`{H0n~;sfB%zl(+_UC*I6e=tdSI{?p$5aPy2S? zaO?XWh`&wl`ne?`y@<2V_bJT+=~TJlQi&hrT^E4z>>u>&8J>+~bo!%BffYUp&j`}z z=J6=GsHn$$#|wP;Lc3V^c=G+A^9|cV-!PZ&DclgTyq&svGhOGf2r8Brc`px{#sGrEsSMRH=VEfAUnB!3Z zQTm{*yi6Utk%NgEFpEVsC>UAs=;YV(XB>j3iJ3gitYWo5;f99cph!>Dys^DI?8Rra z%f@bYoG0ZfTVD=dXuYhJyoXxz_+oLAL)>z1^lz+sfR}1fuVFnx;>oTL23fxxrIW?q zul+ciE$EMpeThsdz?Ex(AS)~_w;MRIQ_ya}|_X}hu zy<9b?z_`zohcR>}=^V|%X8MKdQ8DQrsr$KYPzlYot=&{HF!Itj_2-Os%A>oW-PLL9 zJ(BSJKR|#8Y@>_xg}2op!iHkDzy=l-C##)#YAd5#qUKraJcw+uR+=_1R%Q;}rwIhU zrdp|Qfn>H*-`W6hQGVx2PDeL#vR2GG)UA(v$t_br@{|fK2@D25KuW3ppIjXnrKgiz zWnK%CmrKVGA;q@`g$fAZfY@H!D~C=0U7-7PDn{ExAO0>64F_~Tp|9qo0{$E&CO-Uf z07Gq9-mrE0>r|VIX2ZLwLEqYWRzKyH^%#Iff7r}jj2)fIfV-rc5&Z3JYaEX)S=i-G z5@s9V)}C|I>nE+62Y;%-pCuV%;A~xBh*&c)5mE6zfR?h{ahN-e1hdt!E39f#vsy;{ z>{04>*z3ITm?{H9$Mq(aqDB+2^W+Wfi;tTxz=#()%oa~b+B*^-)!-WzAWS0-bjt|M zF0z@mh(8KjZ_Xcl@PFp<)CUO69yoNr(c(w-Yf&(8$65Rxu@^tp^OMW<+V_#U;{;#x z-UJ`S-tEiq^1rJ2GCn!y2fX$GA%Vl@I^G;9VX}rV*&E7M%?Y?4=Cm3f~ZeoSvLpqLrlnX zN0*Ts)YFleS^&?S+&!~&hV;$d2OW-$yl}F2o+pN$sLSr{Qo08)y$-NfwI}vdCteOg z4FD4m-k)@Sw76)2-Z*@lwC*tSE`X>4jMUO90%tyHVGmCSXNqh6SV=LD3D++n|5VpN z5?y}EpwuF%#EcizebyrH;ir{jTtP;k{eWHn6+0-~GQSun_J5{8E(oSzm^Iwefo>T? zB42i$q>${pMN)BMB96n8wp8>VTG8u7ul^U65W260I*_0>TY$%{;(S36t{YJNCcdWO zfKwO;;6IPg=^5nj5tx@wS4^%^Oh7sD=tNb#0-P{omo0g9jZ(5+^F2hL#3iRDo1hAE zZmBg2Hvb?_cK%i5GzvFPDisr}<)E~W5nG!smHGNdlX(o9Dtmcl0j1bK_W`X;5B6aAMG+;00tszkfP6{=!w47fgYOUFBY^S zwQXo<2-Gm3rJWy4h2FUgC+i^5w$TLyPftaGO)^O@a~!aqzil8UfebmlD)nMcsKd$! zsY;Mz{Tp>lPg+a|U?(&3XL@ZiCp8I*Z^+hrmpAP)z<^4OErp~xKn@d@r03*~y5PLk z25;n*!MI?qcXluDfG$p&t4}@v(#n@!fUErS$o`T(Ii(=cbD{|~AR-osiGZF|*t~n! za-3sAa9`ea04DPqLJSTbrQ*a4c`R(MQ}Ub9BjqTNfXneAS!sFCTTMN!5jM96mM8dY zMu%t4Sf66VL(~)17)Iz$Lz+-q!^LU9!1t{7god`*ai)wI6e=LC$vw8?piOSCO)=bN z2bO;w_M5Qc3YR^>*8T_}1!}bly>l)u6BP$<`Jlyj+S>NzAg2JZ@WsuZ8c@rPw$KE2 ztvQgrSG(XxH;Do5GFW2o1l?hJ9-kRGF)$5msYKDY%O*TDNRJIlKN*`8hi#Ve1)hvA z9X<-CfL#!96@37!e{b}~+$_s*BuC-t_k85;D7vl2(Pc4H_WY;IQib;1>`hip&8q2_6LpWz_tVRSTNWvsE2!;+nI(W`vCR^-{L zbRG06Zsa?en{}{TH)8(%2C`Pb;IR{l-Is&LPN z-X$okr|c8XUMN8zAZ#mIs@zG)M~kp3qs^=02PTu6hbo&mEC&Zg{F| zl)~@Cg|yrVITAJBe3D9^ZFSnXD{+AtaIT4p_S6FxJw*;Tx&hu3z%0H5DC+zs8)IVU zQWOk!fkSLsj}@8+@R_tSGV;E^_{KdaHSoYL5KL#XD--SQ5&eP?1OzZ4Tc^1%8__rZ z*Y5Kec;4NVH0qf+khiFTIVgHGwxx~>nuIvnv&2_>H&q5lEtT0jXaq<%HW`5oYoKZ+zW82zBVSvfH*H6jxSzmFJ8TAHtI5ng=y=5 zXYvquNQzLS3mmc;o@`hxc0&S{OaQX%%-EPc&8Y+=j_tixmjO}5R+K#GtAolTOJuaZ zLR~${bMB!L2i*hBK@Tmd6Op=3W{^S1qfx#NDF+OoUp4WPE~&U7R96#=aHR4?2{m$C z*Xqzcr7i9wq68IT9z}e=^=52D>vM8MvGS_HjI^$QBsmZ@q&k!k4W`C*Qe|*aIC$1Q z4zDc=x3j2i$jWWsqb(a;s=Rma&-K|fTuF{nb_03JluN~+#o69-J5)5HYVoaypWK2p zG5Sy(7euB!Kb!jgyo)h$NGL6lZC4_e(aG7daExINbG(pcmT2xaZ{sS0p~2 z-}07X=2x?pHLhQ6{h*qN`t~v&2^9Z{{I5XMc4(V}0yPj@M?Yn1S{OB1 zK!O`US@KgasdH+eZdF82lY0}Mvyaat-AqY^-Hewj^Wl)egUf9q{R7!|Bw~5COa1Fg zZKp^Rbc%Vn2Wn7-b#q6{bks_d9|uf8OkoD=kSq1xXES)e1 z^)dX+b}QS&T|ho1B|ivoF8c!1Z4}xTD5DK*z8N@q#lr&7$0NZY(7I10&*#;m8=u` zCNblEi|!47yp{Ob4JX{MWcB&cKU99-dTkhoHZNpk2LT<&`2QoRh88@b1VUY6EJqyu663UE2{SkQxn zH)o#9vp%l2SEdZ>kMHK~)z$8(EaBNJJOxXWE$@Ne0v$|IBJ*Eo`XZbRw%pZ*@t@P} zlnE5=R7--~b(8vPXP_RNWPs%sr4yf%yba3N_SLp`w#reiHJEXn?Y~>RshKU7II)I0 zn+xC+yR)=mZnk|&CIhk>fIvknDp4})9+QZZx?S0|%wRM96G*aX#lCG6;E4Y0c&oi> zY_oPFr+c+@F9sef8t~$Z6Yg++SHVrq(_p(F2y@K$wFmH zlwLcxYmkA&E^v>pd@pkjCT{UFKfE%VnN+wunS5|s7i!HIiFJNgY!(<(#YmR>LIeST zV5k};ak!PMu5uRoZ9ti>sz0lThSr~t<xDMQ8iqDhU{ zZcpu9gkAp5RpzwN1hH}GmJGc9rlfQyyKKn4IKOFnR;>#fzdo*8lr1NtTluF^EOxVL z+3*YW=)@^O#*J?kW#hUrw()JN0^4-mxj<%Nxv;e-@C(hvl1;Ea3}{46(<#LqglkD8 ze2Zsgquvy2A^iMBZXP50dbtWUZROI;bt_c4Z#)3(r*u6abF)TT+wL^`aPKc3@GbaDZJwnX)@(^-hUua#kN$j zW+(#;E8sDE03LL)aw1XvqqK*eY6CV2dUe{!i^Ns+PsDGlHtqWIi#VZO5Snv ze-tXe(pC37}e zEq+Z;?PHq7A>>=>bY1@_$E0z+9jP!WR(s2~$Z1P$C`Ln#{(HyoiRqb5XfD9MUda6Q!QBfWE!ti5sQptrZWs)6NyYy1Pp` zG%I8TsE2CBl3gL${jaG6&meg&jtlDO{YmTwl$Nvhd^aRx%S2Q z7}1XEtfM>G0DDudtDS9)7dKh?X)e25T#39drwq25Y~>+YCHxW79wZ)U-K zQk}y`5<=ML=@)%d)xz|D+t+?7`)D+hIK;_3FGt4p`tIi$#@qne==C2!MUx@U^A&IZ zL&$nOnl?x{Yv=ETg4CoLVAZ7o@y-Zau5BmhBdu|2|1zN;`x)eUYJiG3k2zvma@j(& z>aJuIS>F)QdOI|fpMDLUpqTtUE-8sC5;6k?FGnCnjDu^R)@^L+T%QaLcq=@c$#1vhj~K@(TkJ?l~SYfN1tZXyF9qZ7q&O{|tOC8e=Cm zzH3U)8qOj(rPt)Lh6lECL+^;%fu29u*`0MQ_PdgLK>Fp@8rr`gQcZ~R9fw`gOTwi> zgpE^$fY<<~#K9L2{675o*>3In2FlF>N)AsEdO@dt!RO6I<(5SnN5<`Ir~n8;B7_8{ z*JKeb_B&H-SS#V-93vxc{s0XJ=(V*P9@a@gfjRg*z~0`C=WtABU;yajq^~l>BeaF@ zP%SJ8Qs`PwBB!~>r=c_Si`mZIIPKB;TKprs~YN zTfvx3b8h7_tGGFN+&b>uVSQ|^J*Havi>|` z%FZnx%(!LsT+u?+$W}vRuXQ?MIF7KvrexO3nX6v>K`QKOx|$60aFzbg2GkBEXEzqG0z`;jnwhY4EBulB$xdp-+xQ6|d# zq())6ax8IjN+H43yz@#G*W^aa)qxojAnAIRR8;MK1!&n*YW;&J;6CBe=@P3al+SGi z^;*V!KkS`e-Uhwl$lFRt?8G1X>kgEPG8$pu)`f*!kg_~%sL$~DDQLPmZ%ycCt*%$( zg^AB_rpQ z9#dKNc;S)sPK6n~(Zw!N5~TZHYeq7d+0?3`eq40O8~)UMQ_w(C6_mZ~7NM+s8g3*i zTB7?R7q;?dblTJsZWxtp)2&!lU=yGlv);46&nrK(@!8+X^F^BB14Yd@MR+0WYQ;LA zHh$dSeV%G^Y`S<~A=2>wY3{tE;rjZ2KZq0}A_&pLSG0)eod_a`7DALEi57$*dL1cx zjb2AW5WR(oIzkY=8-4WA2cwN)<{tU}o_l}KbDw*E_n&*$Jkv@J7tNO(gXdA-~OG4Hdef6cob))3vniZ@d|_;lpFqe6<&q z?Hhc`;f;bKs3hA9|(UFI+Ar}QC{qJsXBunkjEpNZg z6bLhoUg+W}E=X8TPR>#61X*3{U9e6Fb~Ki5OU#QlZZ$2K`hL_5YxxUv!Pm_%Pl;#z zVynENTm&pDHPBQJ7nY5$i7}OT95$B7N;GR)+;o)&gvl>$m?iQXHKq^ zOU98ZNH&m{=i`%W3nPxy%-kzNUG+5Kx@P#;)wHI2BMadGr1)Eak?vLQtpER%zK*N=g>IrrNCo z?WFkKqYva_|Fiy=`a0@P`teHNO=az;1g~7|f9)5%0SIR5 z)6mGKi&`r%`A+$C$#k2B+|%q~r^2G=lh>C{1Zm^C(kNJDe*jgGf_VF~hNo3>6Ij1Z z!SeJ-2XgaYZ~8ZWAov68(^re9(!IUT9eqACrJLZ8J@UbqRbmF|cdPuupz4?FvdOgC zrGautsK+XF{6K;uP}jldKVJT5_h0_5_2BdW_5|&)bkut?jAwUk*iTu^y>yzs(&A72 z5UtS&rH}1;tF7o)0zKd~ZrIx=xUF3ziku5-Uw4EuEH{HXGW0+omnR;lnz$n9H4GC; zppMVYV$4s$BJD)MJ?S|L@m*;t>sGFpY9+|cs*@urWcV+Aho;cuwm>MLNvv+9$|Fc@ z$KRm@k6XX_4rcFu1zbik^u#evzXjA9|J+sC@wZyZt4+Ii(5*!*iO52Yy0iLYCIrqz zvS|m9@M!!Ue=bQ{#Zd;nu~H!fi{IrMWF_y3MMXevvLj_7VQ#J5_$AtTqnU8admvqN z0r?t@Aq$M*Ly5iIhJq>C=&V<#giC$J?pG2aKGb19%zf6)?Nq6jy13wAT3pH}H$WvVin4v$);7_2m9^37}xRgF$^@lMy7vuHyy}L%?@ukf&-Z z?Kj1|$Y9d-dmHm(!K7i=_%rg^=|bRUMUQBNuR#+Q{qE!()DTKJHT*GVD?LUA`t`D{ z%r;fbaD*jIuHba_^3$6nfg)oC$=wdK2P3UL5W)L8Zls`E(Rp*eR-nzUoDC!o4hg

haN>4Fey);cjN(zR&aNW`YRzamN_NMq02fTXOwaFL})~Wl) z)Y?$24yAcoTu@W{fsZhkhaU|G2@U1_J#x@pleu6<36>YxUb`Z@!nODj9{pBk=dMTE zI&y@E9u$i-Ez)Ql=O$eSlCpVi%>wCgx47eoN`SmMNS_uhExta?bOBCL`I6>csbqQW zdHSP_(S01%<5_5k|NfUs+hx16-UorF(V_HFCSqp_+3pvbkJI*tJ3)b;hD9!K^Ge?b zf6O|0!n>jLlUh5`j>`kqST-fv47AHd?SY4e99mAZor?qi^y+b)d1v(Bk1^L?-=@wSiP`cD|Fx!0#ar?&QQ`~hxst=MA5g9_PbXS{TPVk6$k?jPLZMWIl z$A?HyhLL^CPlr9T`xWk<8bn4aoMc0Zq+h`%Q!LmuUKCruux<*bL|kWTa^^wZDSf~9 zTX%fWveMKj^33&pic5$?s%F49fzugbP$>A0pe*<48spday`z~vwS&cui*y|si$`nI zYD9i~$Wq0$*ILc}HcjwD6k}j{j`9!p<#9!*kx7y3J?VmJB)8&r%Xrv>GD;jLdNQ{X zPF$CjrpATjw-e`OEo0hcG4;ZmI%JiYZ>_WCU1a?^^@bZ@&yrZ+LQ3ndktKzqqe)ZN z+~1W8Wp1m?Jl_Tnr6ra^BD%PgMUT$2sN&vq#!1iqNi$3~)&0+Dh7bQvC)xj#bNJ6E zI?fwqU=fN;tgTMHh5u8WMJuf3c!s<0673A?I!9EOIk8|sI52(hSBtw7cVNzZP_9Bq zzW`jC`AFKbmE@zmXW5R%oJA5abbCinyC6r6VwL$ED8t=Y(Sqi zN~trtnEq&2y?0`v4ZeaCox&UzJcm*D7bW8o@=hW+u}(pslg+&qQmSXpQ)ON#EFZPd z*%oS;BmHV_(4vD!c^ZU@D&UFK^0X4Q{i|WL@6NfI)mLe1IQgc-taim>YN9S_V36HV z4NpfR=)U2ZFDQ`F=H$~J;`H!#>cXUztw+s9x^N&bzUMB)OG)7`?+wL1*O{*^MrV75 zRrrpJ6O zcaz}`o1f~Z=bOiO%N3hGPy7G!?Nfu%R$6nngGkxp_offaj=1gqeq^Uy` zM=z@u3mjR1HT1Uk6%RLlVARn^UwD7$5Z;BV1-RRECzTtq9XTC|l$gRJ>F*j^L3Icf zp^%bKFw}w{-q9K!irHeqcSh|AQeT6W z=r^I9`_xu#r(U5l^3e0R(>NgR$fdE|`bk$` z!r@s@RDWx9L$TLN;(ZWhtILt7B}CD+27QULU*Q}>vsYO1EOu}tm8m&SMq4R`V>^mw z%7~g?9Uzcr7j}Uk6sZ4EzxjQb6Vm>3hL;~(9-iRaE!r1E)pBHUnap`3-tqu-DYHSG=ZG2DC3I}$$|K7(&TJ?ui+KceV*{g zk}4DGvY;c$VKV`P=W`RtX_muV?MAtzft+LRWwRK#+0hnIZwY(K^b;<_K>04d!wOV7 zT+dcVd3{=xIvu&mevernvNl3S8OTW_Rj$p%o0Ur`xwn3=tv+gnq zd=D`~c0=nSaz|P10iSr7D}$>&yWFnmYkQKMt|yk;)f`qsRE~=DPF&zBl!r+L@yI`D!CGm`ShOhU3sUuE*Tn+Rcu*uK9EVV+eHl-q{`2F{kJ$8&nFl%$Rteab@Ft^)U!$MM&pk|kqL%_GB0)V`j6TITg~)GZ!^LiXVN#O?o-j^7*=7}FYzZ*T&wqg z`x{ab^Dx)&QnAGa&tGlOOI#SBdc*11E8;o-+HHS`!+vS+r9S#fuVDK8Q%&vnUUcbZ z&5-zvo*tlZo#n6=er<`PQ)^8UkNP=%a3?Gdv)RzP9jH-asNFN!QN$%IXGQ%SF|hqZ zI-ui*3#uOKeLx2@caK>cJ9?{l+=T!h9D~*(_+#ck%g-exYT=`8I7v`zYwPew$v`P^ z#tZKfX&?j{4OA<>RM+#SgaY(xnCgm#gKfWavkd4#g34`^B7Hs*6^GM17gpw?(-%Eo zH_TJbp`igAjo%XUOv?`bR24|=Q6L<~&eeuEUc2B@>e+x>Exc#M9A&yOln^LpWjAfS zeCza5AzAsq?)3W!4hOC!J5-REEFw-Yx&`PT1fYMQwSmFM>Auyox+g^H{M?jZ5`Jt% zh9nuE^l*BE73vGSp2@n!KFg*Eg=5yH#HdyMmJ#KYz?E_{ya|}v%6XV^^YS6XXxj6r zu0hPP4B1ZiLzWYyC+UUnUZ?2B4wAoc0CPwKw*R>TFR86@^w!glzx@LMPD4r>^Mswt zyp;#&Szn828x!R?vJU#s0Ocw49`{5Xx8^BO`B6m-LGDfjWGI?mfqzU%d6dm|e$G!2 z*Y_rG{(}W*MgW!e7_`cd%Jd#o)cSgsnev0m4dgeg5|7*5ub3Ov7dRcQGwRXVHX!;B zUf2D2^VdQ(`<7iAVCZ40?Op~|1SSjT($iDJ_B=*P5!&yXT;wi53Dp{8a=J}K(s7H- z2kQ?Q1slF8(};b2%GD`&(+}&*WO-4y?f^mEXLk?mU^#(k>Lt>~?Ykc%Gyl5of< zAin;7{@Odc&NI&Jr|^n@lF{Md2iv@CQer5%0L)p9HC-dJ zS1&ZombD&?^CR&yH1gBn#L(5Jl#O^~F>79Cx~s$+2ewG{sS|emQNnV*mxO3)(#Yt< z;sApIolV}aj*~aEFoPfBvXf66Zwm+rIGEPI;1y60BMpq+iyT5;Y=aewJB$?@SJ{X( zp!si?aoln=KF3gvqn^GWOj$3;XN>B6YHIk%u&;gSC*23_>%x|&#Y3e{&1&Hre$j9y&X2*wH&8< zxCtArSS9)-+hNvfZflT%y1W_<%WEWk*9D{G^=lL|38%wZJoiIdZ@<3b8t<0IyR2)p z984u-@)cV@YxlVS)OpjR7F~gTDQFHw!hsOay-(-Zso|jgh&S6IvJ5pX<Ad0c$}1Z`!hygNeocRZcHUy>|NXu`F;s?Ux8TvytAHL57U%+A)TdqTH#Ag z?L`piJd$V!yV!Y~qOj8i68T8pI;+O6k9|i@wBmciYdaE=BdsW=E6Zq*> zLT0XUXrw2V8Y$_nfgam*gT5EcV#xxt#_y4DYubb3tok7QM@+^}hymDX!G z3371E#(L-tc9sl;h2Bf|7AC;RiVD7uASB2-_g)>$w=`^)Xz3J@0~O>XG<~fL>_CCF zHX)rU7SBC($sc<-0L*E}uhWL=l3ai}1*-9$GpDGUFXLhd84B4$0*4H%cL3%z#3Z5t z@az!#&VBRNk>v7}V`J1a@hD+>9C!VubX(qGnh99lLtnqte_(QAz;2l5IAfsSIS{*9 ze#r}5H8Jv8=&qcXE;;C0`h#VeN1N)~ z6Acrz71(GGeQ?`Q@GT@n+UTjV{yWP{f8@AtE8VBKQ7-=6%?C4wVo^5&@5!rLVm4U! zG0MO^U!|L_NbTRsD!bucU+Kx(#h?<>JEZGa*|Wz@u;yM3zb1Ce){HnUAmQ1+-TC)( zVzp)A51O$0H=*zk_3dvdF4Y!SGU8Y$TQ1MH8YR9mNqiOBJ2SOpT+zU3y>5T^-TFHq zjsx1ts~Z~wKz-n7#LbG^H$fL)p|ojVxgLp$0|$_8a|Ge$%#Ia7Y%3xe_u^T5jlsir ze@T>ZoNQ1~8+1i2IYl%oWn`tV~Pop@U1Qz2XJ^8zy1{wi*g;itqlE}^XTSAAUiWN4! zX(8-04OK3W)8UPG`F_tkJ}&e|m%url{JnGg7rvpu9>|)u9E2YnL+txu#0LnG;of3| z=>G2Ls*&VNXAY~<`wVI%Qo)W%HC^pXdDAMabv>cllLiTy#ZfdIllWe$QE^s@E1-Y7 z3{`F>xaJP!F=MHAndmvFjME&7nB~)b%%#LJ4cyc2{oMi`?A{ouK-q=u$(rL4AzwTp z9#|2&<)%n3V#cQ%F8EdzbR*b)o8fhI`NCL7<$P@^nHWr3r!oePsY!BWWA!f8j&U|+Acv*3LtPX=-!77UW(6E-9-N_#r_9d}23>l{Oe zyznxQRDXTLJ9KN=6!l8HVBJX|JItcBep+a5XZL33YV9Qxrh0Dh-kbE;%&dQ1n3jrR zUlGjcEmCgT7L-*UOJ2C&L1@`~e^x>cQo-jZZ5Qq_2;K<%XQ8WOB7G9jwtD^Jb>wYU z6)*8uYlMy@v)Id+-oTIA&ynhUA^X8~k08tFjd^0V0>6nE@|;EhD9OW#hToW;(+j?? zyH7kn;nOUk6%f?P!cEldf0`2I08Aoao^f$K zjpklx@+9u9^T{=?vkjMEmIIo%>cT=xS(2dyIhhhptyMAcOS1F|NW!rGzo=1Ij98l) z{u1Z^q+{*YK077-vAA-~$^`KuJH}<9e1z_3WW}O?xFo?Y`$H zGFJ@yZ)Z(q(^sEdp!!WSV^hzc4KwP0vZ(Ba>A%^-tkHZUUh@U?r_7@bcRE&kLBM0f z(`qXM)?{uc@vjONBWd;!*}+lL`}dZ7LKCtNDhmi>k*MfJVWh0}`$T`SNS* z4KJKAlWL;sW>O0EWlwzTO0HGkt6rU`{7Tty`4$)x?_`ABJ$n<4?KM z+e3Gw@xAaSmtmDG3j(;oeL_-#pi?&Srb36VjFDl^)o}f-=3d<5$1kwML+uFpN69AN z-Q%^_Pyh$)uy{NbVg;AZ7Nu&p`rxv!uUS^xI5Q%=C}Tb8_u<}+oSA2)9v)Q6iMo~D zjLf&-bl}#m)sJ$PPL)mt=aOd0O8d#Wgh2L?XQM&6U%7X0unDNHg^a$yUURfHSH6>8 z=$89g2%Oc>8lBr-(59r>+P-%aFy?5kDzFDVKVmQ00PQ2oCZ;a~kNZ^e3A=sov%uLnQ}{W?9gcDy??z(}7Sn~@fm5nEo$>zz3`i!!XP z^*Et3YHMo)Wsh!7Tvdw3L7d?{Py!w5)K$(t2 zC4Z%C1MUVa5{ukV8p6N#4b{x96f)gLr8Kye`&#&S#9mNCphKr^;iVC-&#xJcKIe?b z?qO>n^pXtl@fXaDq_2|Dj-y57fb$J?%K**_j{F%zF#WL{UD?*1R;KEeQsmm423^?O z7eC1>L1fH(Vb4SeEj6Ibj*&sNNXC27E3Js7*I)G4O+ zy@J&PdCBxE-0f}T8PA>-7=Tnlpbz=o?K*`Ypyr1rB|tW-`>Hv$(Z-g6-mip~AfsWqzFLNIZ3k04)NeO+)7 z#Jec8hnD-|f=!bayv&$cZs#z+F7>j!%0_Kb+Lnv+Ka~pYHYNI(~`U z+~VBQN(i|}fv+(b_I-(r)_G^pbpw7x-Dg8p((D;LYelP*C&)f=5{{H+k(jy;lG=nR zUd}TnomDO{EF$?;;@}9DG&ZnX2n;jLEp}x1tAWIzIFg9JpT8w0Mv1^JJQdy=jwi_5 z(5|{(Gsp@?`)nx()cRE@qOmzzW#cBF^{0H-Jq;yHPKh-D7sUazu*TTOr-2v(KhnXW z*2LT$R}NdAoDDsG0Sq|weDF2>jwi~}Y8I&c0m0Eb`6}*iSCwz28}Ak?jY7R#t1Rh4 z4SYYSFBoHRia&FKONp{?@lPo;Ks+YhT`_CSw~rIF#XLm5qH(t~LeDGOJswEKkfE{lKbCBTf<8eIu0RaQH#Xm+FZMv&>l2N_C#LHius? z0N0LdG@*mcLykAqbD13N*4|gwlqd=c-5ZRR+cC*vOiptg_-R(1`S-F#cE8@Z+pF~$xk=n8v|Cy={ z&f7gA3!+G*M`0E1Q_AqCZG%cxja5Ysj8Rh_%_+8zg%upHZ)KUtZfq3EOAdlL0_L~$ z9L^xG5&A4x#~yBkP-qhC@_S)a>>Td;iR^|}B5P~&P8%Tu{iq}?`U1z@Q68bpZxe%^ zr+2a`qwz7HJkP{6ds}lIZEY){IM2E^i-6-rzIk~*i=v_}qeRxpwAbe)s8y0L0ZtzD z|6uTJuOvfp4uL|1oL7Q_dg_DSw)N_fz>b1*MbcYPOVCHg{nZ(^Il*OvA`AAM2vDW> zX#XT12eFm@4SM_&)Zs;(Y|DMZD_t&L_tM{`I~J;64y@z1&EW>17X>~)_BS8Bdbf@S zXt}>bLGOOn3`L2ayLr^x!j;b5JRqnfeb(uRlE9`_3A}~NG<3PIIA(~zonpnUeH}i{ z<`2O!Z_G6Pdq-t2U~w=O(8VXrqE7|@q9w(Q#;W}UMUblG$j8X z%|uny+Uc>1v+ky*P9`zfvbgoB(A7JLcw9X09W5V~>-gj5VdMS`N@RsRzB!d9n>H1E z5d878$Bz_Ps9k$W*T>2aC2aLWIsIe9p5j|wTEFj3`F<`aSeu*ON;QEfyA9=qH(p9l zOG~>pw#ENsu%~hy{oO#_`Vw^#w^r><@cg^H@b6XUW**_r^k`3ma3x!Z4aOGTZih1& z<*tOt#_NA!`-*{>ik!=_KJnbl!{9?h9K zDhLQ6Zyl_zYWwZ) z`nxG`P}8;d%emcTl@HNsYz4i^oRLtm8v*^tGJSvbJV+wC94r0Wsis}aOw%n~3A0)( zrXwm3cuU;gDm*60{%1JtvhMoOaKZ}ftZzOyz~DaxtNZy|mx=ikQ$t_r^qv&5^~cZ8 zPr!?r+@Co032Hgnux7?WdbC6JDFN3z{^~iYe#~OXWLob=B=?nrL>oRq!BRx+5JGK$ ztw2$K^Ngh>;4F5hr9+_v=oPoX^tc(rx;$Ba3jNk+l?^{$E{-TfrAt0hfaMV*RR(FS z`U4I4+T+BYwI*e>lYr8C?zhRpC#utp+9b}NvHo~xyaEaLd)_8?HO*+zI^`n6W~-A?_8OxgOT z*=bybr3ew=b$ZE+Gy^Q`yArlS!uQ`F8UG}Vkv`Zg=(_y98u`CicUQHVS0Elpy1=rP zOH}{J3I2n_NP%9^{prEieWj0C3(MpXiXfm?NYf(0(&!GbP2LhdFkS`|sD_9B9l+4IqyQ@0Yw2!AfN&6%2H>HXK$!Ya0ewXZ4!ZSB=SdLwv7a01v*qvzUQYJOWA! zWNUu-qse~>`Hz=BzC8b6wT{Zb2mfgVdpRlIP8a%z1mx0pgmPTOph9_7gb`?2>o?&Q z9HL6~Vo$HDZAk#WJhgT;zr+5BJIAH;@s*9$j}@x}oBTH-OoM^(neuY)^ulz$_dVeA z6%gAQt(PnWW_A|1O_UyeHz3F14@ibruF}dOERF!2oZ=RMi)yBue#+mrb8-@+K9(ep zHF`BdHDYD$1_HusLVO}OO<>=)GXX?)(8XolF3Fs}Z96S8ljLR~-UpZ@kU%lV^!j|x z+A_bY&V|c&G=Xs_CyWphEGlS{mlVZMd}#kUdswrwqP?;6ME$CD+?|VORI^kKAwCWf zuJ(;u{El_PsOx2VBtx;ewKa8v3!vD_#)8K}Ak~r*)s(t#{2c#hxU5_2Qe<^2$%DNs znBx{Q?uWfA9^g;HstjC_hvIY$l9RDbMSQ(=)|Oi!kV@{eO&!dU9HlcD2+q)B@lUHf zQan6T3@X%9H?Eozx%!>f-Z;HkEx#DbTToP$-~3!xk8yZxVt82A*~2LDd;`CJn8_S2 zc4FqMn>6DR=w!@P{Xc&iuYqiNZ{q`g8S{ZD~|%-x6& z*xGxX*+n!R0Geu|TBL{e9=AXQxGw&Ro_e7VDRod)$pjq9xOU*um`Ma&9hybIMUs_jR}2#$K289j&@$>KX~sxP-t z{HtN7LPr45g)IXtK#sW%bvfD2+PvDP$h;+{s|CC3=N*kzr|0ktUiQUY-xXrgxhEwy zzLk@cIVzpFBF%CyJlfcDN;C1*U3Go&H=TAYpwFGtRXn}J9MURE;?iuVo_3kSr&}v^4FsrC^$Vmxo{gY zo|LF>X1AUq-jGf&;ufwz5r<=e#rG z3StgbaFIxNwrJLPZ^ACHho^91fu2+9b_Gz!i*d_s>#afF6Q=1fLrP%V=$Lfa^>i2n za_zkqFN`SP{!9_OIktT=86Af6b}-fS^xHmEE+ThvD@QmgSJW&{Z3+!&UHuSo zt@@@n#28}tTJx^RHlzza<-=TL&t%EJr|B`_8m_e^UR@WrnU`mV|6E=5WjOE1X2}BuK;|t?uMP#VMY`SGtLGMArrcrG zfk;4{pm$1XK*wKgf1a>SSFRgOaAuK}>e6O?(3gc)SG^JuYH{@# z3CN92un8=)b1>`i%zMSiItD=|9Ss)WQ}bPjU<(f+vy`;2{DUAs*ljaI-YPV7>3NqF zew{)@lZ+w8hDVv`i~c%0aN@S&vgk^$Yg|0$YsWz6dQ6gaad}6vp&PmR+lhuMbvjYJ zX|q?MmcLB>-Xyx=IYErLr@KR@ErY`k=8#~Q{(^_Kx;CuPQ#N#XkW+{7d@3@rBe1Q) zQm6Fo?A>HmqQ>m*=r7Zhu+0QsW@$lYobETbD5DwI6@q7(_oCZpq`N|*s7DBCs7Hev zJIMHmoy%T64`EkSA~ZL9j76Q&`|~Ie$B5 zhO3e2yDTh4$#y;GZefSu&XWIpV`UuBbh4&<3Ph171@W-RghniS=Zs5)yCs?=5WB0A zoTFAfOVMl`yGEyi!C!7KAZg<{3Kb}AW6mP?0k;WV`_0xZSM@_}Sr*Fn4S+NCcHVbF zf*kylEagEDSPV~nlGH6#d%twXcf~|=Skr)bEi-*;8&<$N@&Ft_idEqO#mdrdCyJN> zZRTlV23cVc`KOOBTpv4toz7n}>%OqOo1Otz*2BW`mB*QHdbg96PBHMJq~y;wNTuAF zo`)hDy|p%yX}1FRY|w(SNUsn2yC*zPTjHupemXa^-sTuMxF~@n0CF~(;tnEnk1Ps# zIxiNuYnr4UL!8@}Bz~XEu zj=zX_14;gyrTt%vLRR_r*=}dJ!h~A1e|k59GGGMv#`YE;KUDcskeQUY3rs&pp+w2K zh=chi&8iPsx&surv99FtsKBK&&v7+&eqTe_80zYU-Y#x{U0mY=uYPhQYBuYC-8OhWM-h|8=e=e@_)JZRawcARtf>OY8%UG z4HtyeFD!+oNcCr{Zg(bAv}Zp@_FmpNeOrT+?=Zsy!&Qst8wy&V__qJU`GsGJu*$l0 z8RFGm-5h$k*}q!^%}OeF>n;^-q5&bhH1>$W#)X>oCTDSA2j>T z#l>~z&}3G0P>LYR@-1ouf9T8zE2zJxl9u94yZ%irc2m#%{g}a6XzP?uYjRg9$%MG1 zv?xNLv}v?@hw{rjrRSdG0zYre)gV^+JN52u*C&)h>n9wV+r5uMX>>O8u6s zXeAjwC~C{kKr!IW1D77@fUG|M{jy5vW5B5+*@;rWVT5}>isZ7a%tuqB=rgQmZnX?L zF22hjytjBSGV}6&U98aCQZTTaKV@$cJ$&7OZDMXEh;iVfWj3jbN~`t%9ZaQ{Ca;akKwH)LwuSCb@G4q@jo3@uNJ6MDb0&NECR%@BZK!70J-I z&4VloR z4DVkL%3t5r$~peJ@@AK_-myD{51+w3sgTSd_jNP1yU7}88pAKYE=&+#pFv_nDeevT zoj$4}9WRSJ&$=-cOsYN7ZrA+Uv+Pn3;#Szrar|# zIcNl1D%R>XE#Llvzu@GrGHrC*|1@^Mrvwi+v-`}nN17ZEl1~GTbzF#E8{NFOK z)=4?QP034Rz-^hEFV%H&o^~yu?lU-no3KBB?FPN?;j3f#wu|rweGdKj@Yh@>9O$@8?3{9E3%hxi;A i{Heg;|NntEC9=m(jt7(eO0Wk3hq|h^O6kKF@BbS*httpi literal 0 HcmV?d00001 diff --git a/py_trees/behaviour.py b/py_trees/behaviour.py index 0cfd16d1..5974b65b 100644 --- a/py_trees/behaviour.py +++ b/py_trees/behaviour.py @@ -344,7 +344,7 @@ def iterate(self, direct_descendants: bool = False) -> typing.Iterator[Behaviour yield child yield self - # TODO: better type refinement of 'viso=itor' + # TODO: better type refinement of 'visitor' def visit(self, visitor: typing.Any) -> None: """ Introspect on this behaviour with a visitor. diff --git a/py_trees/behaviours.py b/py_trees/behaviours.py index 6fbd3df1..c52c6608 100644 --- a/py_trees/behaviours.py +++ b/py_trees/behaviours.py @@ -280,6 +280,7 @@ def update(self) -> common.Status: :data:`~py_trees.common.Status.RUNNING` while not expired, the given completion status otherwise """ self.counter += 1 + self.feedback_message = f"count: {self.counter}" if self.counter <= self.duration: return common.Status.RUNNING else: diff --git a/py_trees/decorators.py b/py_trees/decorators.py index c984e16f..37eeb8e4 100644 --- a/py_trees/decorators.py +++ b/py_trees/decorators.py @@ -38,6 +38,7 @@ * :class:`py_trees.decorators.EternalGuard` * :class:`py_trees.decorators.Inverter` * :class:`py_trees.decorators.OneShot` +* :class:`py_trees.decorators.OnTerminate` * :class:`py_trees.decorators.Repeat` * :class:`py_trees.decorators.Retry` * :class:`py_trees.decorators.StatusToBlackboard` @@ -920,3 +921,79 @@ def update(self) -> common.Status: the behaviour's new status :class:`~py_trees.common.Status` """ return self.decorated.status + + +class OnTerminate(Decorator): + """ + Trigger the child for a single tick on :meth:`terminate`. + + Always return :data:`~py_trees.common.Status.RUNNING` and on + on :meth:`terminate`, call the child's + :meth:`~py_trees.behaviour.Behaviour.update` method, once. + + This is useful to cleanup, restore a context switch or to + implement a finally-like behaviour. + + .. seealso:: :meth:`py_trees.idioms.eventually` + + NB: If you need to persist the execution of the 'finally'-like block for more + than a single tick, you'll need to build that explicitly into your tree. There + are various ways of doing so (with and without the blackboard). One pattern + that works: + + .. code-block:: + + [o] Selector + {-} Sequence + --> Work + --> Finally (Triggers on Success) + {-} Sequence + --> Finally (Triggers on Failure) + --> Failure + """ + + def __init__(self, name: str, child: behaviour.Behaviour): + """ + Initialise with the standard decorator arguments. + + Args: + name: the decorator name + child: the child to be decorated + """ + super(OnTerminate, self).__init__(name=name, child=child) + + def tick(self) -> typing.Iterator[behaviour.Behaviour]: + """ + Bypass the child when ticking. + + Yields: + a reference to itself + """ + self.logger.debug(f"{self.__class__.__name__}.tick()") + self.status = self.update() + yield self + + def update(self): + """ + Return with :data:`~py_trees.common.Status.RUNNING`. + + Returns: + the behaviour's new status :class:`~py_trees.common.Status` + """ + return common.Status.RUNNING + + def terminate(self, new_status: common.Status) -> None: + """Tick the child behaviour once.""" + self.logger.debug( + "{}.terminate({})".format( + self.__class__.__name__, + "{}->{}".format(self.status, new_status) + if self.status != new_status + else f"{new_status}", + ) + ) + if new_status == common.Status.INVALID: + self.decorated.tick_once() + # Do not need to stop the child here - this method + # is only called by Decorator.stop() which will handle + # that responsibility immediately after this method returns. diff --git a/py_trees/demos/__init__.py b/py_trees/demos/__init__.py index fc9d44e6..e398636c 100644 --- a/py_trees/demos/__init__.py +++ b/py_trees/demos/__init__.py @@ -21,6 +21,7 @@ from . import display_modes # usort:skip # noqa: F401 from . import dot_graphs # usort:skip # noqa: F401 from . import either_or # usort:skip # noqa: F401 +from . import eventually # usort:skip # noqa: F401 from . import lifecycle # usort:skip # noqa: F401 from . import selector # usort:skip # noqa: F401 from . import sequence # usort:skip # noqa: F401 diff --git a/py_trees/demos/eventually.py b/py_trees/demos/eventually.py new file mode 100644 index 00000000..d4ae1275 --- /dev/null +++ b/py_trees/demos/eventually.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# +# License: BSD +# https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE +# +############################################################################## +# Documentation +############################################################################## + +""" +Trigger 'finally'-like behaviour with :meth:`py_trees.idioms.eventually`. + +.. argparse:: + :module: py_trees.demos.eventually + :func: command_line_argument_parser + :prog: py-trees-demo-eventually + +.. graphviz:: dot/demo-eventually.dot + +.. image:: images/finally_single_tick.png + +""" + +############################################################################## +# Imports +############################################################################## + +import argparse +import sys +import typing + +import py_trees +import py_trees.console as console + +############################################################################## +# Classes +############################################################################## + + +def description(root: py_trees.behaviour.Behaviour) -> str: + """ + Print description and usage information about the program. + + Returns: + the program description string + """ + content = "Trigger python-like 'finally' behaviour with the 'Eventually' idiom.\n\n" + content += "A blackboard flag is set to false prior to commencing work. \n" + content += "Once the work terminates, the decorator and it's child\n" + content += "child will also terminate and toggle the flag to true.\n" + content += "\n" + content += "The demonstration is run twice - on the first occasion\n" + content += "the work terminates with SUCCESS and on the second, it\n" + content += "terminates with FAILURE.\n" + content += "\n" + content += "EVENTS\n" + content += "\n" + content += " - 1 : flag is set to false, worker is running\n" + content += " - 2 : worker completes (with SUCCESS||FAILURE)\n" + content += " - 2 : eventually is triggered, flag is set to true\n" + content += "\n" + if py_trees.console.has_colours: + banner_line = console.green + "*" * 79 + "\n" + console.reset + s = banner_line + s += console.bold_white + "Finally".center(79) + "\n" + console.reset + s += banner_line + s += "\n" + s += content + s += "\n" + s += banner_line + else: + s = content + return s + + +def epilog() -> typing.Optional[str]: + """ + Print a noodly epilog for --help. + + Returns: + the noodly message + """ + if py_trees.console.has_colours: + return ( + console.cyan + + "And his noodly appendage reached forth to tickle the blessed...\n" + + console.reset + ) + else: + return None + + +def command_line_argument_parser() -> argparse.ArgumentParser: + """ + Process command line arguments. + + Returns: + the argument parser + """ + parser = argparse.ArgumentParser( + description=description(create_root(py_trees.common.Status.SUCCESS)), + epilog=epilog(), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + "-r", "--render", action="store_true", help="render dot tree to file" + ) + return parser + + +def create_root( + expected_work_termination_result: py_trees.common.Status, +) -> py_trees.behaviour.Behaviour: + """ + Create the root behaviour and it's subtree. + + Returns: + the root behaviour + """ + root = py_trees.composites.Sequence(name="root", memory=True) + set_flag_to_false = py_trees.behaviours.SetBlackboardVariable( + name="SetFlagFalse", + variable_name="flag", + variable_value=False, + overwrite=True, + ) + set_flag_to_true = py_trees.behaviours.SetBlackboardVariable( + name="SetFlagTrue", variable_name="flag", variable_value=True, overwrite=True + ) + worker = py_trees.behaviours.TickCounter( + name="Counter", duration=1, completion_status=expected_work_termination_result + ) + parallel = py_trees.idioms.eventually( + name="Parallel", + worker=worker, + eventually=set_flag_to_true, + ) + root.add_children([set_flag_to_false, parallel]) + return root + + +############################################################################## +# Main +############################################################################## + + +def main() -> None: + """Entry point for the demo script.""" + args = command_line_argument_parser().parse_args() + # py_trees.logging.level = py_trees.logging.Level.DEBUG + print(description(create_root(py_trees.common.Status.SUCCESS))) + + #################### + # Rendering + #################### + if args.render: + py_trees.display.render_dot_tree(create_root(py_trees.common.Status.SUCCESS)) + sys.exit() + + for status in (py_trees.common.Status.SUCCESS, py_trees.common.Status.FAILURE): + py_trees.blackboard.Blackboard.clear() + console.banner(f"Experiment - Terminate with {status}") + root = create_root(status) + root.tick_once() + print(py_trees.display.unicode_tree(root=root, show_status=True)) + print(py_trees.display.unicode_blackboard()) + root.tick_once() + print(py_trees.display.unicode_tree(root=root, show_status=True)) + print(py_trees.display.unicode_blackboard()) + + print("\n") diff --git a/py_trees/idioms.py b/py_trees/idioms.py index 1189f5e9..02df7463 100644 --- a/py_trees/idioms.py +++ b/py_trees/idioms.py @@ -19,7 +19,7 @@ from . import behaviour, behaviours, blackboard, common, composites, decorators ############################################################################## -# Creational Methods +# Idioms ############################################################################## @@ -276,3 +276,39 @@ def oneshot( ) subtree_root.add_children([oneshot_with_guard, oneshot_result]) return subtree_root + + +def eventually( + name: str, + worker: behaviour.Behaviour, + eventually: behaviour.Behaviour, +) -> behaviour.Behaviour: + """ + Implement a pythonic 'finally'-like pattern with behaviours. + + This idiom creates a subtree that executes the worker behaviour or + subtree to completion and immediately on completion (regardless of + :data:`~py_trees.common.Status.SUCCESS` or + :data:`~py_trees.common.Status.FAILURE`), ticks the + final behaviour once. + + .. graphviz:: dot/eventually.dot + + Args: + name: the name to use for the idiom root + worker: the worker behaviour or subtree + eventually: the behaviour or subtree to tick on termination + + Returns: + :class:`~py_trees.behaviour.Behaviour`: the root of the oneshot subtree + + .. seealso:: :ref:`py-trees-demo-eventually-program` + """ + subtree_root = composites.Parallel( + name=name, + policy=common.ParallelPolicy.SuccessOnOne(), + children=[], + ) + decorator = decorators.OnTerminate(name="Eventually", child=eventually) + subtree_root.add_children([worker, decorator]) + return subtree_root diff --git a/pyproject.toml b/pyproject.toml index 494a7708..ae206140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ py-trees-demo-display-modes = "py_trees.demos.display_modes:main" py-trees-demo-dot-graphs = "py_trees.demos.dot_graphs:main" py-trees-demo-either-or = "py_trees.demos.either_or:main" py-trees-demo-eternal-guard = "py_trees.demos.eternal_guard:main" +py-trees-demo-eventually = "py_trees.demos.eventually:main" py-trees-demo-logging = "py_trees.demos.logging:main" py-trees-demo-pick-up-where-you-left-off = "py_trees.demos.pick_up_where_you_left_off:main" py-trees-demo-selector = "py_trees.demos.selector:main" diff --git a/tests/test_decorators.py b/tests/test_decorators.py index c9a8b367..43bb4eff 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -604,3 +604,40 @@ def test_status_to_blackboard() -> None: result=decorator.status, ) assert decorator.status == py_trees.common.Status.SUCCESS + + +def test_eventually() -> None: + console.banner("Eventually") + + blackboard = py_trees.blackboard.Client() + blackboard.register_key(key="flag", access=py_trees.common.Access.WRITE) + blackboard.flag = False + + set_flag_true = py_trees.behaviours.SetBlackboardVariable( + name="SetFlag", variable_name="flag", variable_value=True, overwrite=True + ) + worker = py_trees.behaviours.TickCounter( + name="Counter-1", duration=1, completion_status=py_trees.common.Status.SUCCESS + ) + parallel = py_trees.idioms.eventually( + name="Parallel", worker=worker, eventually=set_flag_true + ) + parallel.tick_once() + py_trees.tests.print_assert_banner() + py_trees.tests.print_assert_details( + text="BB Variable (flag)", + expected=False, + result=blackboard.flag, + ) + print(py_trees.display.unicode_tree(root=parallel, show_status=True)) + assert not blackboard.flag + + parallel.tick_once() + py_trees.tests.print_assert_banner() + py_trees.tests.print_assert_details( + text="BB Variable (flag)", + expected=True, + result=blackboard.flag, + ) + print(py_trees.display.unicode_tree(root=parallel, show_status=True)) + assert blackboard.flag