From 4223d48edd2fc10828930360af51636a8088082a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Tue, 27 Jan 2015 14:21:41 +0100 Subject: [PATCH] Docs: Started on a basic plugin tutorial --- ...ugins_gettingstarted_helloworld_navbar.png | Bin 0 -> 26749 bytes docs/plugins/developing.rst | 102 -------- docs/plugins/distributing.rst | 27 ++ docs/plugins/gettingstarted.rst | 245 ++++++++++++++++++ docs/plugins/hooks.rst | 8 + docs/plugins/index.rst | 8 +- docs/plugins/infrastructure.rst | 68 +++++ docs/plugins/mixins.rst | 8 + src/octoprint/server/__init__.py | 3 +- 9 files changed, 364 insertions(+), 105 deletions(-) create mode 100644 docs/images/plugins_gettingstarted_helloworld_navbar.png delete mode 100644 docs/plugins/developing.rst create mode 100644 docs/plugins/distributing.rst create mode 100644 docs/plugins/gettingstarted.rst create mode 100644 docs/plugins/hooks.rst create mode 100644 docs/plugins/infrastructure.rst create mode 100644 docs/plugins/mixins.rst diff --git a/docs/images/plugins_gettingstarted_helloworld_navbar.png b/docs/images/plugins_gettingstarted_helloworld_navbar.png new file mode 100644 index 0000000000000000000000000000000000000000..2f634b0dba3014b6ac5f93e2407b3d509237471d GIT binary patch literal 26749 zcmdSBcT`hb_bx0VqM)FHpj6SLs0gS?heScdMp0Bc2uSa}B>_PMsWt>5G?Ai|(0hnV z6X{YC2oQRJBm@YgkoHAA=lz}ce&xS!jC99&W@=D@}&0pML+z^^+@~KzfGodzIqUiEb0~_%P$@`_*@o_A2%5 zPZ1Zs_GxSRFqfmxRUYA2d3z*Ol=fY|d0@3O-_CJKJqKG(q(?-s;p7Qo1d2NoJ{t!6nD}UGEuIJsP(vw*e@5L;5LU%50`^oetd^ibpv*9#Q zy)U6vbq37U%!8Mz&`;Ku{8yJyPxE%tEPlN=UFJFL<;iabLMyb3l_DYtHo;tdkaMh&lIW@-Y9%j=u{X~_`L94 zVPRpN!uif>;8liUL%h$lA4WOy7DeFkB@w{cnVCpkZUKHZt(p9P$>W3nl1u)7YUcp3%HMI^kVxozqi9?t4 zJ>GHPM)NGF5js#yWWkBx0fknoJ!#C+8@~NtA9D41&)1{}6=%Nenm&$oSREM~L2We8 znalJ`?|3y#AH;fY!4;d9Px}9DQNJ(uB=erL0(Sj+9qCeFtCFE-S#Za;wXfScUhusC z^m!~BFz<}~G)aNaMDgP_F|_2+|E0H_+f=l%ZiLKCLuCH$o%<%d#vHj#+)eyV!cC%` zBO(#|XXO1^f>$6AFq@Qy9E#^10X!V;3Ns)e&j*AK>cGw$q{&lSp!2lar?+6;mmi%&lR9IEMQC z{|$QCnZ#YKvaECek-*K%&D(8VBt{Kb((1g6jUAsY$_mN=GjX={o}<~moyR%Gn>L=L zI_+}mMq)*tZtO$~Wb2eBuC#hpjQHQi%+bwM9!E1VT+Vx;M|>_Q2kzo1rjEs5oe@LT zN9>HkChB&1Phr56De_!`;>0_7OMETT#{DUVxfV>O&ZnSbL~YE}T2Hou^OG_gMD*sDVFBnFS2|OA~>oMg~QB)}uXb`DW6FfntMX9ma@>QEkLdQAAb0 zhc|*56uy{hY9TxgL$JagCH`FXhPSd=bX*37Vd*@Pft>qRr)*=h`O@0lAv>P%>_)hJ zZzXNz)%Lcci+5IabE72kL)Ai6>f;zggx%pZ8CG}4hL>i{%0%( zIlD(NQ2et8tcbJ`OYh({m65?LlQ{M~kd+T!xno<^?}qJtZvx?M`Shuq#l*0+-A&6Z z91%={H{peBcEvf9Le$pEa5WN#gbl=RIOl-J4AmFk{vg?I6vHb{D?U-ZGC{=+)C%ov zRoTG9%-JRi6|V#ps}F|eHHWf1gS-ktKi3M%cXxV2m0DU^JcRX@L*6aPL7FwDiS;!14B$iRW=0YI zU07$}N9LM*P>;bw8{&0BT#>4yA7*PSu4%`cq%y20$qXtaPg>$PL{CbG&iXblPHJ?9 zqE|eN^E8VN0ci_w%JS7#KEA06S{?ah&c#|SfjM=k-KYxNUM*c0kqXCO64wr^(WcHs zkGpqXFst}74I&wYZw={N_E~>B(H!CH+FIDDiO(UgIOkA%v4n&sc83tYfdwvO7YuLO zgR1+)52wI{1d9(L3aAjthSQ4eXrNeln0dv*6$J zK+vAViqOpZiFkd!HSMaC3zJ95H^bnEQc&?PRqv46BkFmg@vn}`P|8Ev_jVv*Sq`{1 zkYzU8aYS{7)$tTDsxrU1Y%x{bKOYyG>4{qkRXG`M{rVmW=k@HGx;NB(cg8zyyHf~? zqh?+)?4nD$fo03I3f7^{Ysx;;w6~HDlYB6VIQrbtT_K|3Cyu;|D0=)dDZP5SZck%b z$in6(EmdiK@vCz~E2KZ}oeK0rn?~`o2cpRhBJq`u8_dx_;q^Cna|&2Etd%9I$h~5o z`rtR(>^V_{i!Jq=XlXuS)YR4*A__eT4Q@o1nmY(;tzUiQVU&LH>#wPJJPpBEECmuF zq~mmHHnl5=2&YWUb^P_AY+SIpHiP~16Zz!ckt@Su$?T6=>C>k4l~bNG&m2$BTLsuA z(47Ud+!Bzg;_FMJ)t)2unM9*Au1l2LC=Y+{QD!whgYJZm?|a4VVdI@pBv4yDzpWci z!rwn(sr6}bdx^X!vm#DiWddqffGemqMU7ZGqgNT$Z26L$8e7opnAWATif%FBdv3j% zi0QN9+Y5Z?QltHcF6LyqAfZ!LwuycI-tseDf17J~m}ak&_g@r|$((kYeWk&9iL|;m z9=_kx3PQJnwdXDBFrj}z^)j+6!$08C3~U`ORXr*S4UEubL)(@S&E0k7*-k(fXpwfl z{t}{$C^&z&%D3)dj|~ReT}%rc0-=kv-Tfdz{HTj0S#o8HglkupvFBvLEWW0rt`vkS z-}xb1Z6rxnU(so%G@j^@Pw<>HPwng=^o=9g8QYnytdEjMmb4e%q;^v9(1rq2svJd0=6fi1Odwm<%vYH~v6UXW=65z`tI{W(Ac1MCAx z?hkGcrDUFDm?Cx__HC$6`b3vmH;ZFiI$+%x=}B`xh*Cgu@WI>j#a8XziX zn*boMtr~@IJL5rMfY;pI7VAc)cH#!NCv|eW8}8(htvT7BI41pB2V{SzH3Ni2b4<+Fm7vRer@QUK#qyR~{_{xzhK@ zd+G_ob8uUUkXK;o`E?oXVK}Uj85&*A{+4Ya8b5Gc7hl~XrB%KtCRd0u;8rHK)!F0R zGE0=YS671c_#s9$O5%kmzpT@B7wVnU7GgqFATItg+3U@4^ep0BR@RqJ^Mi{xXV#pQ zxF+Lul&_$(UUyWbKDx3Ci%emc9#osn#vf?F&$iggg#ujbBNA9`o?Zb<6I#Jxn_(fx zQd&Yy>uRPq%Q8KO@OpjN?!E+%xf!R{y#n44B}d%SQI%lzoEz!xZPoN>PyFQQV({sD zFM%)#dfO3H( z1WoRzZ3_XMb6yG*rY_XG%zbg0RP-S4$de}tEv&qxJC2vVd>XP>okCne4-tLaq=iW* z$faDSK7H?2v62YE8=q?n?YJk$tIcage|$cxKwm@c0NL~ zPqQ0(H+fs{tCc%ywmW%UrQPwkPu@8Y?qc43Or;mz)=$9n>>QBDpp3imcfIS}CZc=7 zriRQ`RS8FMqqyiGYSA|;&T0J4teDY(v?TVctRtP2RsSgK%T1Tq#~32vK~G3K2hX?5 z)+`ZQdTvZO>~ZQW%XR~*mj{5Oq;_9Xo>+bgt}2QT-=;q}hBQ`amvzbB-^W9K{{B=o z^9du-_8kyzCPt@p%%^A`~Mi~Qkd)HJ3Y!c4SK58Ctv&c7`lof zLx(g;5qy#8M4x^$8kMq%Yq?d?dnO0?Wi1^sI@ZSsUD;U?ME&$>P50NNHExPwdY0rj zssk69Pbn<&Vl~1m=LJpMp;Vqv?|Y}Lca7rKii53$#<0o|PaQN9J$Hp5NMguG0IcTpOQQr`*>x6Db2MLIwD zxgF4q_s^+Hfvg|N6j_>41(N}c`EK{E!#H3Xv#?RcZ!mAHgl&Z1|D@69uItor*V`vR zX3-b|nWy5iFx-fDHs_{vT{sXP`c+B|5uug|{;2@*@I+yC#E0E%tw#V4!lK=AtNM6e z{RoZbauTj{{NOllfXsfX{TiI=S0WECc9B|{Y0Ql#jvrsdG0ThwuVIWp-D}z6or>UM zb09Fs6_>GuO6q!sba>Sr|LF4Za7JYib-J&KI-{RbhW4)a%5ZEm((Vn*^7B61JIg$s z8Ms|k8l3Y~#VIrdzWTyGq7UuguioXwVNN<$5ZdC=u0R92BYkwT@b&b`D}BsVrbCA> zDpavK|6Z+uG|pD3f26_op0^!br2}~q0yH(@P3;T}{4)fAr1t}6`WtQ1tO|x#1EiSH zEt^y0I$StT-I%WiG=Xx zFr33W|C&5S6gFD?yEz3HX} zZ^wdnXN*o5{r=;qaofw_^Pg)8cmx9nx9~UnP>o719dg^W=_NcVg7>XrbSiCEO8q7yAu0h*GkKuB-yKK# z`uC(I+qx+BXL_VKJE!jvm#Qx@JSP^Mm=&_(Vre`^6G1b6`h}BUG~+Y;NdV(~0X zka{pn^tURyVMU5FwuUQ+@J2l90v1&l8bhxRrQzi%MFbGd_wgve(kRDu!bHl0tvrnv!RmQ0;fT zHt^;6&e%ipaz_CY`YCg1(uMn&?ZY!K6jry;j+87H?)8LufRlsBp6n=gR-AVX9I9m( zI@skQo{l&<0eP$p@zau`C&R$m9;iosRti=*u@Di)! z5H=STcmr=V)_pqa>^21}tk>8c!8>#;ztgyR=+CsPO*X@w4UyGU_(nS&z@hYML+3g} z^;=x5;0-~Kx|c;#6k9PzR3}PXPDl>mo{!fZj<&uymYhtN49z8c(;Uc4m$wzSJ@2R5 z+f)hioIA2K@pu@PytAmb1pV3NS680pWx9a$>J62@Zerq-;TqJX*M-hwW@3-$9Gx>D zXQFxE!ct%2c^0%(`WpN8)-2OchesBHEs+yjasi$or*%u!-YRj+t-xQd1qXedbG9O^ zl+)uyQ@2AO5O->Cyy=5Shu#jY4w!af(c@R#brrJgkdi`W;}KAKRMU_X^G<-g4%>EM zK!9~{miXM5YP|I%l)g}DH+efVXtpwln`dbAMUY>EO?c7~V1flSCL^?$TsM2JxfMh- z(`3Ig2E85FMSK52JZrzg{tjDmzwg<(M}Q!hfu)G(@p*!5m$Vy`zti!U&qv7`WOy0- z9I)z@P<91P&Tq=g+)2>teoFbBO&5S=A;++EF3|BUX+4`~#w;$D^T3e6Q4kyT2I4im z8Jt${1sTc?T?M!|td}gdS|7}MsM;%bzcr}$9`6J_9F7stT20@qm05)JVdq4O^P(M6 znx>tUJ!?~k6%H^JYh2|rJ;>h~TGkKG$OqnoVLEqaJ>avSa6PaTNkp+18pz?{KX_&e z(rheHML3@q+7+d={Ao+o@(S}e`sn5EY7ae{%=bs=Dm+*7M<0+vf>mto+}%F42YTL~ z39!*2c)*|-#}x$CVWc}GS0>NZp}l(IeZElIkY$$6Z)AFH{GfuZnwTd6lCap`3*cv(<9# zm<2?X$ARQ5Tk+Ry$`ifVr_)5t6d>fwSMm@`9{Z`~Ilu|=eGOT=TPo(K*4KMpBehV( zK_$GnQ9xV}|?81a@vyRNXA`Lbie3-{Md z)ubcebUtgt@vAIGmEgN;4OC=>PZWe>p8&IK{3af%1TH?Z)ZYGy4av$6|wvY+k-x`dGYS|~MGCo`n{u(rOdf6e(+LOpFP@T`;S^F(ApYXj+pe~v` zg$|OuII{Hon|BXG6E_&>rnG`Hn=W&d?$M-csyo+>bPWY9^9t4qDKz$-88E*W7VUz& zqeA$+#IkDxT;!+nozx(oDlECW4d#8Xd!ck7kIV>Su0A*c5jPk@Nc-ySCIin8EB;v)PFcbp9Pv7viuOvWNmxJZ&IwsL4j5-MiQ!n|?XEE6*!#=Wm}B zA$j!O=HdrB-&)BbL70)$9j?tN?JYtS#N6^nW^|C4k68N?}Vp8FKm4>h^&cpiqxRK2;uz8v-PKC?#MCp4wH#hy+ZbfectuF%HIRpKj^Nz;5U=Ps(`go`!&^AleH&iXZzt%Pl%dSca zE&mHgWsI_m+XyoNI?5*YU{CyLt&xp+HtY_1AmzuHKDp!!8`WwxNSx~~x!ts%F1AgJ z3O%#A-CLkiIofEY(I6KN9&t>WSS@q+?1!d~U76?Uap~Zp^RR5U3O4zIEiTtAMiRWTX z{(KwxDS(L`3ej8A71p?v;U^=Y;yn!o|7D$fjJ+0&$lPY5x&V*-TOSd+lIzi%(q~as z0a4Zu@-S)7wT{0Q>umTc#UAHSZ~er|7%TU%={45<5q74D$5A9w+cjD_g>R#%T#?fA z+OeTCMFw<>xk)~$-lc@7J4M1FH|~A(@f-s8G2Lp<=jJp4;1O_by`KkoAsmh7_tsIZRSLOxprO>TsmL5)OS5(R}+z@KoEv%n3l_x(w`vjT0 z3ANsekQ6=+L$qx}fIY2klQmm*%_kG-KWT_6^~`U$&Tx;JS2T^4CVgo~%)r1Q#=GAA zL(KeEirHDiZ`+0ALAb-T?Y0agb^JJ!@YY)e=DTz+F@bsxCbSX$t#5oQ zMHl-v-WpJzy$$>(Fj1YhJu!b(vr4NKhvx6PA|gC%Tgw#G{9L|a9zHivB~6;B@=8u> zoXadAji1BR4#22Zs@iZqkm)F1uQCqB zrJNr?)_MXpapj%HR;d9M=TMV_{6+(7IgnZ7%!?7$tP+&LSP0U54Caz8RDKrs7(dWo zZ_*&eSc$(x@{?S^uAfq@?y5?;tU8Xj1X@V34Z0WNK~s6JyI;))cIO}gVfV-w$zN7S za_(>r$R3!xDySuB3#|Ux1-Eo?I9r#nbgug(Ox&&Gy%?orwM;T>_0!)@yw&3T()`@- zg@`0?s$+q=l%j!@GU7Z14Ff_t#JgRz&BN9MbUx3;X2e$B=JB2LuGlVWIV zE`S#=JXq%-h(uN6Sq#%*RpsP*&W6m2zGFnlJAE2p3JiMBBlC7AS?*|~!N9buES4jw zdG-Yw>*w3O&5;|jInGF6%@tc%_tF#u)H-%Ah~sDXr9lH9L;057hr^NY_&0o;-jNz+ zKe@kY)gvYL9DukFR|h#O$o=v3X}#Tav|H=jEZlSfYLcO^RNM_#4(zJ)uyS!wka`>| z@s)Xh`a+)&8{8lnGI257KBm*NGKrs9o4_k!um2IJM_qn6Ty3-{-^}iPM*)PT8pt9B z#2uMGknUxNNvGP8D72@FEk=B*>bn!E%`lJJpn?mB_X+T>mSsAjw--086LHSY&P9dq zfmPBQx_+UDTQM^F!W38>g#2oNBg7J{Hfe|Wd3T-1fw0zPrJ%<_drJ#&Z3Jw}lHl!k z$%-M?-6FTY_lG-$!MMe;R0O?2@h>mY!jc=6JT{Gaseh`kYWl_1q7A zVF^ur!R-j%mS8-d$k@zwx9h8s7VaOqFT&YoY|}@#$AYxCA3Xa#1zinlKdf)2Kk>#; zqg%ymGMy7|Vb6lJz+a@i3RqNobE+h9#W61o)R_*PCVE>_YVnEj47dN-$j0$6^}xw7EC#QQR> zJ|5p9QiJe1OMEE*h`WNKQk9NwUr}F3jY(YA|ww= zInjyJJ>(S=AH)D)tON-d(}xM--08$^7M|BKe0!oJXv*IfH*CbDqX4sEu2Rgb6kKqH zWJNH*D~&RqO($1EXd1jyA#qGZ1WJ=TA`J<|??S_o;rM7?$?0CBv3FE3v6Z*J1wxhS zYavv**LB)0()R<$x6?7}8tV-hDL6rUc0z%O=^|r-gK@QVR*=~k+gS5N={Q3pyC`A8 zJ^Ke6hx{2Da}tDUWv)tNK4^~!On57z!AR>(V&0lPk-tf6d9c{_Wmhb4u+)8xqFzd4 z*-4LA2YBZIcwi2pvVH>df>YAt9CMD((FNFtBcyGn9pDtyz;pH);ZyuwOH9G~R%uow zJco#g4V@ki7ev<^1+XR!33pD#ca!KH*aE3{_{p2SM#2)0%2z<2-Lwnzur{jlvf`Bs>OWSeW zm91en!%>w$H%2JwYq^>=ZWok?8NH9J|oMF=+0f?>{j0Kys}Qo z$NJV+CYH**q^j-4uZZs$N%L`Ot_BZwsGQmH<`Q9Au7qKjDK*v40Uk+183y8y(33-= z6P4XzrX`1@he2f`oeMl$i6z6~rfK{emzq>=#85|XVLpTkR3&sRvo!_Hw!GzcG=KBw zWi^&J41TvAqX(AY;w5_9N~#P$QoZ*qqJylyJU9o@K1h+@k1Y)(x2M{6+e9q>&?m(B zsCc$u6UV^K;sSs{03%QPHt=j9%jo6l#AfiO}DFEdtXHbzm2OBO&|0iz~S{-2R4I0$k_I^-sJK6r%e z42lRTQI>D%F%+yS4u{rH2a*1`y(t87J~RManlBGjgc0LR;z;Fc7$L0YT$sMZI`^^d zUhIEjn`ohlnnSQ)Xuam`@RtO3-jpZOdO8cfv9V?yR1F7$j3wuT*2$;nC@=W5XEWq6 z5NBc(;SDFq{JjjWf8i$NMhjg2R}daOT^JJrpa0`CB!vE5aVC=6gUzI<{x22PsLkx^ zSVAqbH8AN<=&%l3^V|AC2qw8&FoN2)^dDhyo@jK1o1iwtIc1frQxONm|0M*jXDy@vd=g@jY4 z(Z8*kL&^xcWjYKXn*^*zU>Q!?%svbpLuOpv(E61mvCR5oa-5g*dJJe&uKcS*?)7ZC zcW@7%`ve8QcY5TN98!nus8VW8D8v_ekDZUkfI>MKBS;@}l$65dj8c@t=>* zW~zAp;M@2R`YTIih{;I{;CQ+*q-;w1-^nuh5C)UIA{oCyaf+d|gbp~%KPcJ9nXJH% z;zavae$m>sl;6~hCCFYET>3k8$3>jg#h$F~)1z_=0_-EUkx<11F9ud+&GV28TvZJTEMiucO2Q-2AcH zrlyBm-9|r`vN^#8*r?TSogM$9Gw)Vf;Xj!s6Um295o_j+7#7xgROY;NMT*4Mi*KR7 zY+y*~kC%zPRRnzhPnQWH^GO&qFGc;EQ(st<+u>IG4!-8CsvPID8C}1ayf*i5#Xw~n z{=t?!OTHJ3c1*K1tWUn@osw5 zxsC(^cQ{2(g7^!d?C3veO0zel|AQtFKx%|NVFN>y2a8-ve~B3;pz~`8sIjM>|2wNG zdG$~4$0Hys3;$Ifr0N_b#5BD5cQG-&SDXhw6zuo!vU;9Rxkhx(@V|<+Ufy%E&Sdw$ z%cqjFBZD?^n(F^9fk$J+Cis12|6Pj8KN#zV%dGvEJR=W<-CPvgv!MSjP4WM~2su@9 zC<2=m?9ua1zRq|4#;=k6=!DAQ{JNuXCK+AlI(){m+9|2b20k&=7ilvn81@eytNUWa zn&`rF5YXeaZGVayqo_ z-GG@yJONue8*?}0^u&$Jp20FwE`)`PK_j2P@p5^~TeyK^FVuH3whaxgi4anKDIdJ< zCwhs7hH-}T-{O_69L&73YOd*e+k~a@ArG*tWr4;g5WvM1X%)>C$emlp*RPe$!qS)?p8B-S`{<(45CsS)X z{^z7>uhMgtojfjWtS4ljyJ6oMc}_-XQG^d!1?XRMq1X1khT25LhS(_0O+2vW5HViM zvr-wiw=9bRg}aZe zzT0v8oQ9JQXxjWe^t!3F$bI?sJa!DO>Jt5n~&k_!#$$UyjaXhDf8PK z<{s#3v(&v7+t~RQn00udR`SyaEq<~^J#_czh#qO$z2^ld_vDLJe-1pnKwhzjW?%_$ ztOVEjc;wbmbKjV#78ob2X~l$CflTGw>r1O(R`uVdN@!V4$f^{E1j#we4f%BTfZ`6a^rNd&P!s?1ynC-?N>fWy|D`=Fe_JpI-~I(6xas=@DF`bvKwn zA+*>V;(&`S00Lho^x>z7iq2VX!pL9_dNpD-_ug1KPa&wa1iKJ(6^uJ5F`W*FXj=Lw zPx(xo5Fa&Lwae4WmRNcW?DOx83D%AYsk&j%I&njC%rtHB{hZZRJ9-ndWBSLmUAcGC zl6uq2X$yBzYl_vi!|JF`s1VaxxOgaggpgC{2}npbXMymowC&KSbB zKJ{p^rOV$_|8w&p?*!=Tu%1eAmS31@+x^Uq8)kR>SMq+YE(Tp+8d%>DMKjt@;;dO) zH!Z<~-Jinm7wf?fdF$nJj}6$>MtU7vJW}=leH;hNGdCUi09HX98gRiOYm&jMp-!LeyE-b z+DJ>EFpfjY7ik3Eb0zBexoyz*H|9L?|H^(jQ>1m^)+cSi^1{f$b0Z&Y;_6Sn_L1yn zY#&eHi;{R+uqkLi!Z*g@|3n{z@@=AkFii2YRa`LCTec`GGwi-h_R-?Jvz|%b;~z`U zds>g&k4ppW&NfMCT*+Y_vLZDj!S8e64a>zt&J|3ODfu3;XUjv@Z_USzSken~auQ)*_yihQPT@YbhVy#AyJ z>Z5!?T4kd90AgJ1`9cJ*`IZ#Cf9(0Cf|Lc47i2|7mk?T)Z_Y?r<`!}=z_UBY;0+6;x1jRB5i1>9jznZX3Z^spKiiJP?>UOe`fKrouy=$h)=__kLj zV-v$Aw)KmQN7rVp(68zq%GjtCoy~wLsGsYM+uZ2cO#3>9nWz{)V_*Y~umw^n@4~1) zeyhiY-R>#2TMmn-MyxLy2=EM;w^aq-mQHe=@1j}kO%bE$uFr?t$}gpSnwj_Sd$71Z zYS)?t4f0yO6{ChU(k%NrXY{)M_V5ymMKgGud%dAx* z;K~PXMYPDj_Lbz!KZ6+fw>62pSm8ulx8UbG*puLq`Xw#235@5NLPGy>!o)8y9>zIW zAsihOQuOL8Qr1o)Z*P`XB0u}NMJuePnQRS_sQD{p%Z}p5`RGrTzV&5*)YP{R@YOCS zo5mqcK&FcA+#PJ5+vI!cOYGf_1Dad7kDlzOnH=e5me<({Qoa-mG_>PWy$5MHfe`n+ zU@Tq!5ZerGS;u&K0|%~$nAZhRHHfaQFFR*K%91w;3pM^GDd*cgqYavtABc}!ltobL z;FpWO?mZ~xc&F+^jW#F0drjpWyI|{QO7=lMp|@_6b+Dq7p@!!wCUAGwYVJ^7!f2XP z1v-r9n`w=AV7y7^na3>6zKHEjg-psXRd$Fe|5?BVM^1+pS|}U}oR@Zw{=o*1TUfRh zw7-Ir^evlRVyuPBDanHLtdqm{*W}rU9+XJCm!xKv3|1|U1%LOq^VpGFuS~?H1H&mm zJe=_*cguYhu6uac3ObVEtcg=Dh>vjyClBpd6v0nrAAKsEP#}Fc96Yr7Q!zXZQ9Ws} zU~w;dlwx6R=sBpD<>$t_*YZ=ayju{Hfk1`DhE_}aMPCMvaF+`FUWz|(mf~YEA(jcm zj2N}8O%3r-{d|J{%n7ptt~Cia*Jy-93kGHX!1xz2_Tn8h-^NvMM*rFYt!0F>n*GTA z@US}~fwyby0!b0-uNV}uOy{>Oxs+f>VAHG8QJFthX&w`j3%pQy%M{D@M1Cq7&y`#2 zjo6TIvz39gWl>O%S@bGAf-4j0H>FbErC5#d8F(yyb3<~ed3p6#-Ny|>@c=iHCJNSj zmTCH=$n8W&J*&C)D+R^xZ$U!&C0%{^3-Q}B0-hu-lT)H`6Ad@gOw>LOq_;FMFcOZm z^>=zIo3ujw;B=J6h2g6*6cskbfQJn>FPpS_6>{-41A{$&`f|B?#0u9P63iSM=DmSQ z=lQH*q&nz+_b7kY)qe&xI&+NrC8`PxCMO>qqk>jMTki;7YkFZ-e@jNH*vooEwup4* z3R~58JmOksS>iWpcLGQ6=0q9((plQ~CNv@1&~PeNM}GA;OE$4AwiKDOzHu~uGAl7G zx%C%KZ8{bBhF8SU5dG@ctQVpGiw}RQ?&kMZZ2!ZeSNDZ-_Qn4pwi7xwaUA~jr#=1e z99mrTKZIedfcr+i8u zwOja=_2y1{X;^?^vnb}51fpP!@Uw}g(VUYFT9*P>N4mIy^IomArB^4~e-e2?iZgSO z#J%f#pbO#Gz69@X&6>C5!t#QYxOj)CBtqoNKt*@mcLDc(@ zcd}F}v`tZAy86vsy%8+%YZ2%UQ^ zy8M7IRN@-sgz8zvLzsEcA#BtK=pigmr;=dkbM3_#7hPuj@v8d%hQa zzLn?QJY6}rve+gFWRyHCdDJJcIkk3K=&+g%XrqHRyRN3OycNr}Oy}iVhVITQRx5sR zOqhq|&4D|oRZVz}-NZ>i#F89?Z%&9*#^(|ryg%C#jWFoKaB{uH4x!~$y{^B$ZEsXm&!R-@QU!zkR>y?#;RKFBJ3aFs&EljLQ<<}+ zwU-f#BLX|595zGN{zh(SOLO0GWnKgWnfm%wXSs_5qr! zrQ`KjV9DNalRGpHblppX{m#;>(Tx*@6vA*En6>ISO-VRyDHG<`sc)3G4oO#HoKCa) zzOjTmC0(4eD%oi^VJqrTW#U-DThSSz=rskw6BiE&pDobKSnu946jbcemhr57$0y9QXV9j)t3ieDl(!dU>>cZXTw*5TuRrnUsCn zZuKYisoKKfyRFUqhrh+}7d_wO<3AJgA@hsQ2|J_peTef%9-J6Cf8gz*pI-*j{Vw_R zt7EMgTf;^eNqmHPf4_jPO`Lc5ZN3~7c0&(zH>kucb>hNIAn4dD@R0$vt&6eK_ioN# z=c8$ZA6nsN+M&JH0U>i0TSi<(dqL_m(%jU2Vn&EtEwVhIOI@P70n$G9$wGb-($|m; zMz)<7c}bU4aWgraw-%zIVGn!_llcG}y{A&N{1gSPyWgX9*Wo-88Jk|{#MKG}btaMJ z@V#}J)OUiE*OR6gF~U#EN_kDf9K-MTnc;ZPnbl}q$>|s1F2P0hQ%@RJKbMq4W@AJP z5uD^}#6Yb2!xPiX`}8QFxd$&Lu_Ie8dICeC3Dn$b5!$WLD3rkgQ!it~;hsEpS%md3oI!#s2DjBmJ&(X^WZ{z<1JYYN)N{fwik3-I)#pE8(bUm z%?qCO?UJN0jSo3ctv3biA4Cd;sGB_wIVRB=BQpb4`z#P&N%BhpIQQiM+5Mi@FG<(Q zoTFXGM8}3Tx4#>Gz02#vQ$?N0{II+~Phe?x?DDYMZ&5F=3*6WLDhj^t7Y9A4j$ zVERi){$mgRmx1n;3J`wa`re%HcK|OWEQPUo#vqSy(fICYT{tdLvYqw@%A=shIYFs7 zFC-u%y^;D1FS$$xSh>V9?YMs;q{?@(-hqc?Q)!{U&L`=f^=Y|?xmMgy%JE3Ek~mfE z2qf0fEqSYy151K7yl~I`O9UQ$gs86Q}sRyB}OSrS1Jn zGhq6Dt8uw;_KD|0J@YNb=R}1UpQ$NclsXH18U9^2hySMk9?)0YASZq`EN9VZJ`KH- zjbBy8Xha_gPXS(wg{-TOYiWGmdVIYpe!kfr1T{HM8OS@qMv}juPwW^l69VVfx9ukL zqqHL}=tS-xF;w;s_RLAum95#xwoRJG^(puv?2Fs`>J-b(tCv=CXYi-GIftzj*{AV{ zF`=Vx6YcVs8&LDg$Xos9uR7nPdLG7ySfmNh?6D1Li&VSt*i7PtuVK_vfdoGr$+cx& z+fL|4uhPKe;RCR9wuNoNOA}%O&#KE?Hh+kJWBkQ`a5_Wbo}Io_=*v*6*?8IDeBS?zy;EgO)^w^uE}% zKAK3FMBtT2ufEW(-_6zE+K#ttdmgyL39hPEs&))L7DZiXE#Gqs|ChoMS(BWu!iqAK zmt!?5&qGB!aZrEbE{~l5vSbX(JK<2*vCfN+cx5+o+unfoa#o$KO3snuK5hy&%mu$xQ09njuBG3?$@OpSd3$=0KJ!9McfpvbuIBSz4|u-y8@bKX1cyX>FF3*z6tL;5DF(VKAPbi4JU zb6?Y@zo$*O7I%tER^C0T^fJ$WuljXtSp({f{wulfLhW9Qyo=ATNS_sKkMob(jPm35 zKMTC5s2&Fr#_qEh**FiV&I{kl?)Y^0)D5djpV@C4v-qF#z;D^Y(>qso173Ko>-*`x z>MKE&Z$18awiU<7r5eH`2YfoE)V!x#R7Ume4rpD` z5cW*gGUz72Hnvzh){;+kw}AA9%<|nVjctY9Pi9_xiGGuNVE3V?_l?f*ynVpGM?t%L zPgq!-y|30eRmg{^C|{ie$7er0fgaZDijjg{3fU>X*%M{$43ECaNw*p+-EU`@y{~{r z&HWqqjaa+cm&dM29EcPMp3D~nA8#{TTK2it-;4oy0DDgZUMX+tmG4m=yY^_MZTGL~ zQasxIYV0QUalhG9-GD@D_KthY*m(;hcQ@@$+=;up#_~!p4$7NV%&@|CTQ67p4t>x| z(UUH_clnFLkfQ(P`i0Jn<-50bway4a_WwA4Xl^d|k=@I^kj~;`j}3W3$8?=HbR`19 z#yDa3ondA7vAe|Y(Q$-po-ZEW8|u?Mtsz_xs~XV7^#XMy+I^DSla=g$x4nDM!wAxe zb&aT#rxhQpci;AXaMQs#UhQ3#bjMHw>OQ0W##@O5kQ91uCTi+)acW;@Pzh2m_OQKJ z(C1*`pK7mkAV!#)1`)x(H(ADCo^na=p9y|Q{p)u0~<}n|WPzfZ4AdmP2|h zEj_2!OjK%ycX~!Bd-2Wpd%XJvq!vzr-stXcOo-;A9e_U}&ICQOv+0V~wTF1{F6f8c zAr<+ZZA_*A$X_2YBVUzhFGt1l1>z5`tHvc1P$M6uFmJJ4Ti=3@c3V}xY&4IqHG89- zU`lOMeYw9iYT$cBJV@?_9A-ficY<2wQ!*DDNfZ*gzu<;;jpNpqK8@Al9P2fj7JpJ8 zZP{-4N><~lY`k?PGgtSh9m*cWYP0EXTL$b!Ht0+5%tcqdKnW^k&d8XbjXUFks<86{ z0oqlE&g~pz&Inl=Lme;p&j`*5PMf{iR!@7J7S?cfJp&hixXP5Op(*?kabVLTqq;)( zzWDU=OU{5%^X|A5Epa5LKJI9{^QKT^lvbX^VeH<;XSW-#{u(j69}6t<`U$#!RM&A< z*%WW_b29OI48$ie9WSkN{s}!C7zX2Acyq9sZ)G!hd!9O4rs=Gzp$s9-bSTbJ3jd; zHS<33q7HQ~H}c->%RPIb4|s3nwnfU1{H8Vrmn*7-)Cl zg@E=4AocpR8X#HM*g^71O#aMY>yb)lUv}_fj#+U}@@{;!iY2)j1CxfeGE}WO!%BYX zi?8nz)wt6pez)XK_ijmP|NT$bj_;nbWo@R_+lDTt#ux#aTiTqR@Ht><*VhACpVuF8 zMqBEE^i1WyRL45I9U8yFI8MrazPpW&7@y?qRbi1Z`^c+N2h3vOLKPwDT#Cy_vVQbe zYo~~-u1##?kOQmxjV9zeK7U+pmB-5&9$LN|don;odU_7^f%*Z_*bm4!!5Q5&Q0c{UfqQ#+f8*7%{kAl zHL`c%!7TW2kU=hP%R$2bqAaOe1ow@b@kJc;O*P#3ZoJ@;zsDax`?_@eon?M)dtOhL zORbbdvY4`)%Q4%5XkC0H=or>`(a8R|vA}6T-yF!+|5em`xFz|x@86CTSK`1`?uiq( zxK}uH&$1k#SxT8BbK+jO(6lV~N=;3%wB<~|k(rq(q;FFkX;F$PsX2c1eLlzW`~%zv z2VB>AUaxE1SxLz3D;GrrC02Q9b$1WW_(mE}e!cvn_*lwxwTv(++|@4qP)jIMQ!mv2 z$M(A_xyTsHU$7IP>*PD~jK#f{X1>lOz)dZQlKWT8;WH=N@!^)XL-% zi69m1s_DljEWgcg+ldiKvTJhfwGdb8T`6}{+tPmp(1cEagf#7eyf+)(2oS4e{O z7h-jC&C-Q43dc>UK#FTHM~ovP4*7edc{;7g+e-az zLD-?L?pDgA7ieLix7nDAQtMN>?Xq(Nb#tf;AQ{Q#%aZy`CG2c)ut5EV+i|JQX-phe zBG7;%df4t1*m6DH97Cg@Ly34i2jR|aHm?lI^{3c{ysALKL#5mPHKW;7SuWb!g>8<) zwnC`@cTrh=BSMyyJ5+f+iqRLm~1RLGOvkVrxhL>!X&Ys@I0QmK{*Ov4RU( z;vXdayr5d+< z8dN7@ zhYN2X4a>zGCWtDyiNlkmoci@@?sB6xK-+t2gH7)kfG=2fTDMO*YU00th*=Lr&BCw# z^vQz6Tmyz*oHl3rDq9s~P}@rs(Yd0uU4Vl?YB;#tiq8gFeWYWzB_*-T@zJ-8bd|kN~mieVC3*%-eSxkFItc zH|jbE`d}v>(s9VW|LLjy`gT^>lFXqvNmtBU!SM#+OO zXgeU=?rk$5>o}14%TA6YDlOy3a__EB;O~{K@dO zkH&*r*WC;W04%#P{YVPMOSkq417tdg3x9*cP(G9?+$wzmR!1q{+w-@@XAOo@1bml^ zG|<2afN5J=0_O82bG>>xSG>O(a$RWiy`F$yFXPBuPc!Ef0)0C%U3`gQ_rLp2G22DI z=>=u1#;FVeh}ur8&P?1mYI~-roAT2O?KR5$s^*RMHBjXX5Tpb$ZB%Kj^5&Hh6hvy-@}qy=hAoBjKo5HLBuw>rMPs`coG#G5ZY8 zxeE==&s8PTNb{CgcSHK|ni=u?;NG_~npwbEGpCLFkIKK4pELRk*lrBJRp(rGc-zF! z!=h74zhkgveX`P!7eMwB8Q$*!&9jLGn{C5}G)xv0$N1FM#dSG&oK^jAGojC%QBPH| zgH4^W=PFsknVY8wnv>HxLR>XrIg2_Uzb@ngFHEFJKP=Z-_5nzxDzjFLrT%)e)$f;X z;m_(MmBy<_R2S%pnxR1~Rd!m8S`C~SfeJgIC6 zpOrwL()6d6dj-iF)GO3;qY9|@ZsEd-C?3L?GK)bVxKf6hA{+}68+`fiDPCg?5AkEk zI`CIRmMVHwalk2*ku4kpfTftKu?9lgh8Nkrw;BGavyy*fbb)Dbin191g>1A;F>ah{ z`c)WmZ^M?2kz{Q&`~~-5Mb^H0&el1HXOtHeKfwGBxh&PZ^tx zR1ledBZNka_RxdIW~Mb|>}A$i{!S}V%M|kUc*Ko!)_45n3c#rGTRG*pb7KD6atR!# zt`%sYYRiA)YUUpUE?`w@J_onBu6miapM`(TiOo)2J6&JOa%A+|VlvtkwCT|pefGHq zb?!sL#CwAs+n+)v-2+g0sX3_p=8^+NGIL`oe{xE$N76JO7(g`8?Ra|;CTJ6>7hCRK zsH%k0(9<pkzie}uZf7nCFt46sA$Su{c5KIZ5;f?c`^cz3DqK_%Ew?HyHEFw- z>MdNq!N{m9U<#T_W1w)BOHT?4{KUc60LSm|{Q9v_3-^g`f%{-D!&Lry_F7qup4FF* z-%UY9v0nVyG<^UphyBq&Q-sTxkobfhOC(dg|IHYTBO;LrAF^BcUY*tCq2)KGeT3L- z;PLZ)o*32m3~K^7NlPN~8TXF4?`XlZgH6j!t^>wklgXyo@wNOq)z#pdBRguV=y^hm zU=2?}WZSOdRlDlx7K)j(#Nj)&GG_H^I)R_7=mL^5Cm+M89F;Zkt111mZmS+qjvl|V zBQqm1b}l@U`2HS{dN}e*>OIh4K+$>-n84dg);j<9{|q;NkNvMTH}O1`+0Wc+U%Nj~ z0>QbjjvWv`&6i4?0#@Qq#(UeOO!3y(#QmZ$@&g06LBo^gGV%G|sD@{-wx&4p8@OTr z+upXKSr{>IHF7+ah4a?IKRg`qOAtif*Gw#ebY();gH(4jd|UcXgUx6_$bO6g2P`FO zF>7ow_FLr(`YECJ-_q{A_kMBfKO+4y1uuOOwtsyg@&o=M&vgoy<`GoAu3=v>@SHH| zOb%nP8I{eK(fVH^z}pJeGB6blJ`QD+5om!b!l|1fCx*Nx4;i<8koO&T=r-*+iK%92=bo*UgTRyjjLCK_)CN~>)g^mYz zgSZg?pG@GDL+cb`%#!S$LY-^NFhUc;wD38<-6Heo~lb=ksxoao;k(_(JC1s@H-f{dj5c7z*wa7V57+0Iy@BVn@Bw z>l!NLnb$omoU!i@VOx*Dp3Ct>og9une3tYkm!T8Fp5tj`H3}z7kc7a`|6YPjUUa(gShU+4)vimVMkIg3KSftI zKh}2q;YBt#v6!5>+k3{C=$Yz3_0r+_6U6=goxzU%|Drk^<9tzCC>&grq@-3W6O~zo zWuct;JM?(PQn=r)*DG9(UN|nyncw$@Bn?i|ixGDQ{@+0WsyVv}7BKi#@_l_?KpPrB z8>*nN_=hodylR5CsL~V+vu~PVZVHT5{GV`J9@(vLGlz{D?h zwl(QlH{W!jeOJ087kJpRh?Sw`1N{+1(L-xp93bJGAE6M-vX*IgqFjkTtSc42^Auij zR2qBd>Dyn~m$s$)mdc4uaD(1=5~RXineyO)Qh0p(zT-{F*k$`sj;}DseL#P9LU!YJ ztM{MHwRaEvPHFRDVH`>Ph$XRCZy~z4gzZ$+)Pvnio|@wqQljAeTyXVVX|9qKJ{}XM z3uSCV^&>5--dF5d+SBuQcyxacYL7dl$&@4dpc@SUqY_>PAGw9N#(eVmvG<4$!}hy!mx6za}0kr zt&`xwh$4~SHA(9iPzKML123_Ew@kuc95k8$lo{sNnuR}?!MewLhdSJfYC&HBt`$`sm_c)XN?~hcfj03SZJ#qMuq@N zBf#0e!AHsux&Z3|5)g8SOAl@~vKW4o zfQNHb=591aT8lRQP<$x>D5{4Gzb+D^TWHHIn9WaUPXGxLIwsUO4+Umh58gQ(RHn%c z@D&<3VZ}oc_i#)U{^n7JD_Pe22&Zkl5vBaqB~Fn5{tcc>(q5~13uB*G?RMn#ORhYr zvtK&*%17T5xLtyHDD$~7*^%XHeIe2!dPp?+;LO!e3wijbof^oq^Ul3*<)7e{1JMZ> z#T`3D<|GDv9+w04!^PHJ_RWqo@2-%!l-R8NS0Sq!;ufb&yM|b| zZ>(lWtPJl%BHGplu>HxqD%C;%i$|xNOT>ZQ)ap?P;y>`?CS8f@95-WIuNw)FVxiw% z+$Nb7@YC!_`7XvU%o>6x%a!5V@2*U|Gr%U62#mpQ!TqDI@swLBvZb44YDq_>=bNpu zVco*Tq`Z`S1cU?N=vPi|plVf`@? z#6}!)cVZ}^ZRpGPhbp23>xV@|hO|tefMtZtZAl0VH=Wzl-k#gw7GGvbd;9XXo&JsQ z&m+7=B>pL4U`$(zSf_mGuqCA=H6~9i=NRj>o7In@6JdFd5VD6OlhX}#``UO#?H)li zA+@gCY$1z^U3mh{zw^{gfgW&jS4Bl0N`4Z?_I>hVwAOUf^r!o2huMNP!7aVP z&K;TBQ=waJBJ*6Y+pc}1yAo9&r<=`-gW?MQWbTZ3UM0CTZ4vdPglxo30il2G5XJIY zDegD)+mW?klU_vvxTj(1HJs-kwGJd0ZB*+QRU=vFn^_Op&NaPkA-3$Zdpl*XH1Br+ zzF79oK{J)i3DNcmdHgao*?HBjdFeN2UQ5h3Q`|A8GZsejRm#3HzJ|uy$8$>ike_Bo zzM;6k1%KF2Kf{ZU_>FNU=*cM^-IPjyOJY<4+r19>snXMuH?P#HCH0z}2Two)-puhB z)lu%H7FTYTaPdY6g2=qm4J1qbW}-Mu>8ynvDkse$l3LEf{ zkl=qI_4LO??Jy*I>e+bwb!RV%CK)7C=-NpziEyN#@K?R8ziTtvhYG%Uj{ zHR@cSW+M#1=)ZfCl>jy`1^5y*GkIyxeE>axtiHSY^nINioqNW8Jl!8cEuU%W;+$`A9zJn z*aJBXj;u!t&jz?KDx57~V+7kMIdiZ*@#T4W2E+lnj#wk4=z3~)@i{-L3Ei!QtibDo zZ|(;>QJh#iEHQldiv_az(RuDE%m?7>ovcSn&i>A>QKCGqO^t;WrPzs~E96q0DL`nQ z@~)tv+|d}rvV{1@);tV7ee7oR7CjSHSl7|LNL>)59}NOOsux!mG)WQlGJtSIjTZGG z-i$KFO7qM4k_<3?LM);rnb1*I^8P3qE*U5jkBv~NdhN2O5HC)kg{D!7KYStxu=uk;*Nx8to0JjVvcJrMvum^^1 zs1~TgmQ0Qr5W??78IH9ScJ94rYkMf&-=UGLKTTBZtlD>w>i=%u2z^ADBHk($Ma-8b zd360J{U)zAJaxTQXmda6gs2Bc+S|?XLC7w>BdCW2*#m{R>_kvyZ77n_XQ-g_2J-y* z!o<=t?roOitE>Fz0JL9Qim^W@ELzgXrZ!&VG@1GZ5XnTlXVD`xFY)_1{zR#k-tCj? zzf_;?wQnJaJx!CYev&@plyU^6-8^`6WLm%;-jb-2W9@cUn$9#whU|)7Mw3cT1TIs$53nr zSpxy76sew-8TmlY>98mEl47}Dgwc*N<#k9;7%2u#^c5M7Sr2fG2~b^4pD1}zq#9xM zYt?2)JN8-gK6y)fq}WfSUevz7@Qz}lXX@dUX%FYRE$=)#JuI*{XZSnNf#@TU7Dx|t zK4dNTCX9M6yug{h^{_jMYK%v6l@D8E@)LYeqJM9zp62hdG$0OCR$MDP?;n9omK10X z2SMKpu_$zutc@48pB!5Ja23C><1;&Qy2MGcQ5%0?Q2`_YdNLB0#j2KdCJlquUyjxx z(JUf3e?f$WzBJvoMKFje-FZUX7O@;u<;dfMh7PJs*3{f%wcanFzC1tWsW#|Jc7`yz<}++9sN(2pDQqBha7mI7TEz*4v@sDKbr`zYYYj^X|Yutbu0KHfb6z2b6?jy{}f zI^Yi}XVew^Q6vYQ=IR>iBcS>~jSeFFPaN+SbEh8CjHYDT5%}UWQs{ZcG*@eyx4~B6||W4a-Xcb4!1M zqQ;_?NQFG}NZy&kaO_o_u2g*X-nk#HJk?~KbvGkH(hHgupm*Kbi@j8oXVJ&Z{Y$*Q%OZ=UwB&^6600M=#H{_St+KCz9B^~pX1uV9^@Sz2 zb5i4sx8=`MvCD9N3>59B-Yh4rU;N3F(PwR*fJ=xzWL3)jB zZcnKs@+mAqPeA4x?AI&EZWUah))LT?xd|W%?XlhN-3Y}mx+dBN!>DqZhc>>+`hm*D z)yX$!EEXiLi&KRxLj|SQ+kLP55tAt%x;};fF*VU#R4m7IX&>rdrcX|FQ@H3Qv%rtF zR)g%Z;61%Lw`ymudwpYSA@~mIW~bMHpO%2M9o#9!Sk9AuO0-4#U=ufw?p3LhBc1d1CwQw3;pKTBO123m zq=D7c3QXl{IiZ6Xoj#KpBGtoAf+6{_s_xT``=E}l+OiL212M-lo^J8^v3!uWtO`u| zj;?{li>QNmer|{GIQ9n0fm3j^OAw8;h{R9z5v|~VuRHZB6$jsKD)Vx--NK5E zpI~yxsR@@%@{C8bLJz{w-V)<7J6;DNlqT$ZudY9Khm1FCW3CicEHR>F%ZaA4x!SB) z1(Gm=WQp{u=@nj*uOw=xIWyc-udU!Qhi^dLxI~haHcDFR28B6|&q7_3OC({ql=H4k%ASzqyq*jb`6aE=Q|v#HlFr{OGp1R@18J`)0ao{$BgIZwB#J@~56!_sN#R@slY--IR8JYhqLp-Bc>R^c_??pyuJi|aoxL%SoD&uJlQX}~-l#H1_b zFVm$=@(l4CXWt}Q9GFkt*=zrHQBj=qVpaN~?PL0}nI7g4moKK>IrL997BoM`g^mZm z_LW2bZ8+-W?kX}VlG?e@Kh3GHIr(wbZSDGT<+}IU9CU>KE1#PY!qJ@BAL(@ik@Z?D z9!|9}5?{)9)eK8cQsC(c`gG%ZN$%8PPj16kwQ|%6(PyIvi({;WQ7f={vJzu3%_31* zPC8x1*AOSC6H89Bh3cLOnnN#w84(N6BJPO@CH?znuF3{VKIVy5*^W!CFDsNfF+_WB zD}J%3ie6T@uN5CS03)cy;v;eeE(bOgTle1Tqu;EDRBIIOwh$2U2b5o+E!)3=ABvj- zl_bxPS<^e{KYs0?572)^x4MhZ;-fEAIGZVRP&iiYy3mHDH7JWmY@jBxx4=m2Oci3l z{^x5OKMR#=cSj9v->F^=*$*ICBBCL(2~2M41Al?s{(>s`Zt>dUPZux626gW5lOvss zU-)nnS`M*ide6{e^U7XbYfCmjl603C)${9)O=aqE`r@S$HYR#t^GeZ`Wa~>`M}y!aLDZJL-2ItuX>ok8(P@Dr)i-7J5@ecc zoB~I612-jnnuDx4uAs#@LeR?L#U2YSnJ-PJjL97kFUVT}Y9lq%L zFbZH7`y}(GIrRv8(xAM_N>a9NI?#tT9z7BDX?PdMQ`nvvb1_7sm=9>_sKIDc*9;3$7QQsYkZ;hgFyc<`#mKP)n;82*uG|c4k>Y2u9sRr7;FJ2WwN>XC{AH1GF zcDspQEB$U2Q5YHZ$jxnL9Ba`wVrMfl!_uJT+?OzX;$tE5m`;(XLUgk*Y_e4C3ccG+YO#^WPPl^hzRtQozs4J8;gY1F;8K*B zJW~L&JcGY498Sq`nbET{(6gC?kUx&y2(FWsuvuQ65pfUn`I+MO%|&S*YWn!#S^I%N z!|xYDKOfGwM7G!vZkjvQmHd(Zt88K5c>Rt!^QuuqTivi4IHKP!*TAF6-M4${vpddG z2ddv)il*35Wee;T(PU?IjCk#J5Q`_ providing -a couple of properties describing the module: - -``__plugin_name__`` - Name of your plugin -``__plugin_version__`` - Version of your plugin -``__plugin_description__`` - Description of your plugin -``__plugin_author__`` - Author of your plugin -``__plugin_url__`` - URL of the webpage of your plugin, e.g. the Github repository -``__plugin_implementations__`` - Instances of one or more of the various :ref:`plugin mixins ` -``__plugin_hooks__`` - Handlers for one or more of the various :ref:`plugin hooks ` -``__plugin_check__`` - Method called upon discovery of the plugin by the plugin subsystem, should return ``True`` if the - plugin can be instantiated later on, ``False`` if there are reasons why not, e.g. if dependencies - are missing. -``__plugin_init__`` - Method called upon initializing of the plugin by the plugin subsystem, can be used to instantiate - plugin implementations, connecting them to hooks etc. - -A very simple example plugin which only hooks into OctoPrint's startup sequence and logs "Oh hello!" would -be the following snippet: - -.. code-block:: python - - # coding=utf-8 - from __future__ import absolute_import - - import octoprint.plugin - - __plugin_name__ = "Example Plugin" - __plugin_version__ = "0.1" - __plugin_description__ = "Logs \"Oh hello!\" upon OctoPrint's startup" - - def __plugin_init__(): - global __plugin_implementations__ - __plugin_implementations__ = [ExamplePlugin()] - - class ExamplePlugin(octoprint.plugin.StartupPlugin): - def on_startup(self, host, port): - self._logger.info("Oh hello!") - -.. _sec-plugins-developing-distribution: - -Distributing your plugin -======================== - -You can distribute a plugin with OctoPrint via two ways: - - - You can have your users copy it to OctoPrint's plugin folder (normally located at ``~/.octoprint/plugins`` under Linux, - ``%APPDATA%\OctoPrint\plugins`` on Windows and ... on Mac). In this case your plugin will be distributed directly - as a Python module (a single ``.py`` file containing all of your plugin's code directly and named - like your plugin) or a package (a folder named like your plugin + ``__init.py__`` contained within). - - You can have your users install it via ``pip`` and register it for the `entry point `_ ``octoprint.plugin`` via - your plugin's ``setup.py``, this way it will be found automatically by OctoPrint upon initialization of the - plugin subsystem [#f1]_. - - For an example of how the directory structure and related files would look like in this case, please take a - look at the `helloworld example from OctoPrint's example plugins `_. - - This variant is highly recommended for pretty much any plugin besides the most basic ones since it also allows - requirements management and pretty much any thing else that Python's setuptools provide to the developer. - -.. rubric:: Footnotes - -.. [#f1] The automatic registration will only work within the same Python installation (this also includes virtual - environments), so make sure to instruct your users to use the exact same Python installation for installing - the plugin that they also used for installing & running OctoPrint. - -.. _sec-plugins-developing-mixins: - -Available plugin mixins -======================= - -.. automodule:: octoprint.plugin.types - :members: - :undoc-members: diff --git a/docs/plugins/distributing.rst b/docs/plugins/distributing.rst new file mode 100644 index 00000000..7a8d6fc0 --- /dev/null +++ b/docs/plugins/distributing.rst @@ -0,0 +1,27 @@ +.. _sec-plugins-distribution: + +Distributing your plugin +======================== + +You can distribute a plugin with OctoPrint via two ways: + + - You can have your users copy it to OctoPrint's plugin folder (normally located at ``~/.octoprint/plugins`` under Linux, + ``%APPDATA%\OctoPrint\plugins`` on Windows and ... on Mac). In this case your plugin will be distributed directly + as a Python module (a single ``.py`` file containing all of your plugin's code directly and named + like your plugin) or a package (a folder named like your plugin + ``__init.py__`` contained within). + - You can have your users install it via ``pip`` and register it for the `entry point `_ ``octoprint.plugin`` via + your plugin's ``setup.py``, this way it will be found automatically by OctoPrint upon initialization of the + plugin subsystem [#f1]_. + + For an example of how the directory structure and related files would look like in this case, please take a + look at the `helloworld example from OctoPrint's example plugins `_. + + This variant is highly recommended for pretty much any plugin besides the most basic ones since it also allows + requirements management and pretty much any thing else that Python's setuptools provide to the developer. + +.. rubric:: Footnotes + +.. [#f1] The automatic registration will only work within the same Python installation (this also includes virtual + environments), so make sure to instruct your users to use the exact same Python installation for installing + the plugin that they also used for installing & running OctoPrint. + diff --git a/docs/plugins/gettingstarted.rst b/docs/plugins/gettingstarted.rst new file mode 100644 index 00000000..a4371e92 --- /dev/null +++ b/docs/plugins/gettingstarted.rst @@ -0,0 +1,245 @@ +.. _sec-plugins-gettingstarted: + +Getting Started +=============== + +Over the course of this little tutorial we'll build a full fledged, installable OctoPrint plugin that displays "Hello World!" +at various places throughout OctoPrint. + +We'll start at the most basic form a plugin can take - just a couple of simple lines of Python code: + +.. code-block:: python + + # coding=utf-8 + from __future__ import absolute_import + + __plugin_name__ = "Hello World" + __plugin_version__ = "1.0" + __plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint" + +Saving this as ``helloworld.py`` in ``~/.octoprint/plugins`` yields you something resembling these log entries upon server startup:: + + 2015-01-27 11:14:35,124 - octoprint.server - INFO - Starting OctoPrint 1.2.0-dev-448-gd96e56e (devel branch) + 2015-01-27 11:14:35,124 - octoprint.plugin.core - INFO - Loading plugins from /home/pi/.octoprint/plugins, /home/pi/OctoPrint/src/octoprint/plugins and installed plugin packages... + 2015-01-27 11:14:36,135 - octoprint.plugin.core - INFO - Found 3 plugin(s): Hello World (1.0), CuraEngine (0.1), Discovery (0.1) + +OctoPrint found that plugin in the folder and took a look into it. The name and the version it displays in that log +entry it got from the ``__plugin_name__`` and ``__plugin_version__`` lines. It also read the description from +``__plugin_description__`` and stored it in an internal data structure, but we'll just ignore this for now. + +Saying hello: How to make the plugin actually do something +---------------------------------------------------------- + +Apart from being discovered by OctoPrint, our plugin does nothing yet. We want to change that. Let's make it print +"Hello World!" to the log upon server startup. Modify our ``helloworld.py`` like this: + +.. code-block:: python + + # coding=utf-8 + from __future__ import absolute_import + + import octoprint.plugin + + class HelloWorldPlugin(octoprint.plugin.StartupPlugin): + def on_after_startup(self): + self._logger.info("Hello World!") + + __plugin_name__ = "Hello World" + __plugin_version__ = "1.0" + __plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint" + __plugin_implementations__ = [HelloWorldPlugin()] + +and restart OctoPrint. You now get this output in the log:: + + 2015-01-27 11:17:10,792 - octoprint.plugins.helloworld - INFO - Hello World! + +Neat, isn't it? We added a custom class that subclasses one of OctoPrint's :ref:`plugin mixins ` +with :class:`StartupPlugin` and another control property, ``__plugin_implementations__``, that instantiates +our plugin class and tells OctoPrint about it. Taking a look at the documentation of :class:`StartupPlugin` we see that +this mixin offers two methods that get called by OctoPrint during startup of the server, ``on_startup`` and +``on_after_startup``. We decided to add our logging output by overriding ``on_after_startup``, but we could also have +used ``on_startup`` instead, in which case our logging statement would be executed before the server was done starting +up and ready to serve requests. + +You'll also note that we are using ``self._logger`` for logging. Where did that one come from? OctoPrint's plugin system +injects :ref:`a couple of useful objects ` into our plugin implementation classes, +one of those being a fully instantiated `python logger `_ ready to be +used by your plugin. As you can see in the log output above, that logger uses the namespace ``octoprint.plugins.helloworld`` +for our little plugin here, or more generally ``octoprint.plugins.``. + +Growing up: How to make it distributable +---------------------------------------- + +If you now want to distribute this plugin to other OctoPrint users (since it is so awesome to be greeted upon server +startup), let's take a look at how you'd go about that now before our plugin gets more complicated. + +You basically have two options to distribute your plugin. One would be about the exact same way we are using it now, +as a simple python file following the naming convention ``.py`` that your users add to their +``~/.octoprint/plugins`` folder. You already know how that works. But let's say you have more than just a simple plugin +that can be done in one file. Distributing multiple files and getting your users to install them in the right way +so that OctoPrint will be able to actually find and load them is certainly not impossible (see :ref:`the plugin distribution +documentation ` if you want to take a closer look at that option), but we want to do it in the +best way possible, meaning we want to make our plugin a fully installable python module that your users will be able to +install directly via Python's standard package manager ``pip`` or alternatively via `OctoPrint's own plugin manager `_. + +So let's begin. First checkout the `Plugin Skeleton `_ and rename +the ``octoprint_skeleton`` folder to something better suited to our "Hello World" plugin:: + + git clone https://github.com/OctoPrint/OctoPrint-PluginSkeleton.git OctoPrint-HelloWorld + cd OctoPrint-HelloWorld + mv octoprint_skeleton octoprint_helloworld + +Then edit the configuration in the ``setup.py`` file to mirror our own "Hello World" plugin. The configuration should +look something like this: + +.. code-block:: python + + plugin_identifier = "helloworld" + plugin_name = "OctoPrint-HelloWorld" + plugin_version = "1.0" + plugin_description = "A quick \"Hello World\" example plugin for OctoPrint" + plugin_author = "You" + plugin_author_email = "you@somewhere.net" + plugin_url = "https://github.com/you/OctoPrint-HelloWorld" + +Now all that's left to do is to move our ``helloworld.py`` into the ``octoprint_helloworld`` folder and renaming it to +``__init__.py``. Make sure to delete the copy under ``~/.octoprint/plugins`` in the process, including the `.pyc` file! + +The plugin is now ready to be installed via ``python setup.py install``. However, since we are still +working on our plugin, it makes more sense to use ``python setup.py develop`` for now -- this way the plugin becomes +discoverable by OctoPrint, however we don't have to reinstall it after any changes we will still do:: + + $ python setup.py develop + running develop + running egg_info + creating OctoPrint_HelloWorld.egg-info + [...] + Finished processing dependencies for OctoPrint-HelloWorld==1.0 + +Restart OctoPrint. Your plugin should still be properly discovered and the log line should be printed:: + + 2015-01-27 13:43:34,134 - octoprint.server - INFO - Starting OctoPrint 1.2.0-dev-448-gd96e56e (devel branch) + 2015-01-27 13:43:34,134 - octoprint.plugin.core - INFO - Loading plugins from /home/pi/.octoprint/plugins, /home/pi/OctoPrint/src/octoprint/plugins and installed plugin packages... + 2015-01-27 13:43:34,818 - octoprint.plugin.core - INFO - Found 3 plugin(s): Hello World (1.0), CuraEngine (0.1), Discovery (0.1) + [...] + 2015-01-27 13:43:38,997 - octoprint.plugins.helloworld - INFO - Hello World! + +Looks like it still works! + +Something is still a bit ugly though. Take a look into ``__init__.py`` and ``setup.py``. It seems like we have a bunch +of information now defined twice: + +.. code-block:: python + + # __init__.py: + __plugin_name__ = "Hello World" + __plugin_version__ = "1.0" + __plugin_description__ = "A quick \"Hello World\" example plugin for OctoPrint" + + # setup.py + plugin_name = "OctoPrint-HelloWorld" + plugin_version = "1.0" + plugin_description = "A quick \"Hello World\" example plugin for OctoPrint" + +The nice thing about our plugin now being a proper python package is that OctoPrint can and will access the metadata defined +within ``setup.py``! So, we don't really need to define all this data twice. Remove it: + +.. code-block:: python + + # coding=utf-8 + from __future__ import absolute_import + + import octoprint.plugin + + class HelloWorldPlugin(octoprint.plugin.StartupPlugin): + def on_after_startup(self): + self._logger.info("Hello World!") + + __plugin_implementations__ = [HelloWorldPlugin()] + +and restart OctoPrint:: + + 2015-01-27 13:46:33,786 - octoprint.plugin.core - INFO - Found 3 plugin(s): OctoPrint-HelloWorld (1.0), CuraEngine (0.1), Discovery (0.1) + +Our "Hello World" Plugin still gets detected fine, but it's now listed under the same name it's installed under, +"OctoPrint-HelloWorld". That's a bit ugly, so we'll override that bit via ``__plugin_name__`` again: + +.. code-block:: python + + # coding=utf-8 + from __future__ import absolute_import + + import octoprint.plugin + + class HelloWorldPlugin(octoprint.plugin.StartupPlugin): + def on_after_startup(self): + self._logger.info("Hello World!") + + __plugin_name__ = "Hello World" + __plugin_implementations__ = [HelloWorldPlugin()] + + +Restart OctoPrint again:: + + 2015-01-27 13:48:54,122 - octoprint.plugin.core - INFO - Found 3 plugin(s): Hello World (1.0), CuraEngine (0.1), Discovery (0.1) + +Much better! You can override pretty much all of the metadata defined within ``setup.py`` from within your Plugin itself -- +take a look at :ref:`the available control properties ` for all available +overrides. + +Following the README of the `Plugin Skeleton `_ you could now +already publish your plugin on Github and it would be directly installable by others using pip:: + + pip install https://github.com/you/OctoPrint-HelloWorld/archive/master.zip + +But let's add some more features instead. + +Frontend or get out: How to add functionality to OctoPrint's web interface +-------------------------------------------------------------------------- + +Outputting a log line upon server startup is all nice and well, but we want to greet not only the administrator of +our OctoPrint instance but actually everyone that opens OctoPrint in their browser. Therefore, we need to modify +OctoPrint's web interface itself. + +We can do this using the :class:`TemplatePlugin` mixin. For now, let's start with a little "Hello World!" in OctoPrint's +navigation bar right at the top. For this we'll first add the :class:`TemplatePlugin` to our ``HelloWorldPlugin`` class: + +.. code-block:: python + + # coding=utf-8 + from __future__ import absolute_import + + import octoprint.plugin + + class HelloWorldPlugin(octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin): + def on_after_startup(self): + self._logger.info("Hello World!") + + __plugin_name__ = "Hello World" + __plugin_implementations__ = [HelloWorldPlugin()] + +Next, we'll create a sub folder ``templates`` underneath our ``octoprint_helloworld`` folder, and within that a file +``helloworld_navbar.jinja2`` like so: + +.. code-block:: html + + Hello World! + +Our plugin's directory structure should now look like this:: + + |-+ octoprint_helloworld + | |-+ templates + | | `- helloworld_navbar.jinja2 + | `- __init__.py + |- README.md + |- requirements.txt + `- setup.py + +Restart OctoPrint and open the web interface in your browser (make sure to clear your browser's cache!). + +.. _fig-plugins-gettingstarted-helloworld_navbar: +.. figure:: ../images/plugins_gettingstarted_helloworld_navbar.png + :align: center + :alt: Our "Hello World" navigation bar element in action + +Now look at that! \ No newline at end of file diff --git a/docs/plugins/hooks.rst b/docs/plugins/hooks.rst new file mode 100644 index 00000000..1b15dd4a --- /dev/null +++ b/docs/plugins/hooks.rst @@ -0,0 +1,8 @@ +.. _sec-plugins-hooks: + +Available plugin hooks +====================== + +.. todo:: + + Needs to be documented \ No newline at end of file diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index 25cff1f9..76b0621a 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -1,3 +1,5 @@ +.. automodule:: octoprint.plugin.types + .. _sec-plugins: ####### @@ -15,4 +17,8 @@ additional slicers. More plugin types are planned for the future. :maxdepth: 3 using.rst - developing.rst + gettingstarted.rst + infrastructure.rst + distributing.rst + mixins.rst + hooks.rst diff --git a/docs/plugins/infrastructure.rst b/docs/plugins/infrastructure.rst new file mode 100644 index 00000000..da6a6e5b --- /dev/null +++ b/docs/plugins/infrastructure.rst @@ -0,0 +1,68 @@ +.. _sec-plugins-infrastructure: + +Plugin Infrastructure +===================== + +.. _sec-plugins-infrastructure-controlproperties: + +Control Properties +------------------ + +``__plugin_name__`` + Name of your plugin, optional, overrides the name specified in ``setup.py`` if provided. +``__plugin_version__`` + Version of your plugin, optional, overrides the version specified in ``setup.py`` if provided. +``__plugin_description__`` + Description of your plugin, optional, overrides the description specified in ``setup.py`` if provided. +``__plugin_author__`` + Author of your plugin, optional, overrides the author specified in ``setup.py`` if provided. +``__plugin_url__`` + URL of the webpage of your plugin, e.g. the Github repository, optional, overrides the URL specified in ``setup.py`` if + provided. +``__plugin_license__`` + License of your plugin, optional, overrides the license specified in ``setup.py`` if provided. +``__plugin_implementations__`` + Instances of one or more of the various :ref:`plugin mixins ` +``__plugin_hooks__`` + Handlers for one or more of the various :ref:`plugin hooks ` +``__plugin_check__`` + Method called upon discovery of the plugin by the plugin subsystem, should return ``True`` if the + plugin can be instantiated later on, ``False`` if there are reasons why not, e.g. if dependencies + are missing. +``__plugin_init__`` + Method called upon initializing of the plugin by the plugin subsystem, can be used to instantiate + plugin implementations, connecting them to hooks etc. + +.. _sec-plugins-infrastructure-injections: + +Injected Properties +------------------- + +``self._identifier`` + The plugin's identifier. +``self._plugin_name`` + The plugin's name, as taken from either the ``__plugin_name__`` control property or the package info. +``self._plugin_version`` + The plugin's version, as taken from either the ``__plugin_version__`` control property or the package info. +``self._basefolder`` + The plugin's base folder where it's installed. Can be used to refer to files relative to the plugin's installation + location, e.g. included scripts, templates or assets. +``self._logger`` + A `python logger instance `_ logging to the log target + ``octoprint.plugin.``. +``self._plugin_manager`` + OctoPrint's plugin manager. +``self._printer_profile_manager`` + OctoPrint's printer profile manager. +``self._event_bus`` + OctoPrint's event bus. +``self._analysis_queue`` + OctoPrint's analysis queue for analyzing GCODEs or other files. +``self._slicing_manager`` + OctoPrint's slicing manager. +``self._file_manager`` + OctoPrint's file manager. +``self._printer`` + OctoPrint's printer management object. +``self._app_session_manager`` + OctoPrint's application session manager. diff --git a/docs/plugins/mixins.rst b/docs/plugins/mixins.rst new file mode 100644 index 00000000..b59d88c8 --- /dev/null +++ b/docs/plugins/mixins.rst @@ -0,0 +1,8 @@ +.. _sec-plugins-mixins: + +Available plugin mixins +======================= + +.. automodule:: octoprint.plugin.types + :members: + :undoc-members: diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 49f5702c..a96263e7 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -421,10 +421,9 @@ class Server(): pluginManager.initialize_implementations(dict( plugin_manager=pluginManager, printer_profile_manager=printerProfileManager, - event_manager=eventManager, + event_bus=eventManager, analysis_queue=analysisQueue, slicing_manager=slicingManager, - storage_managers=storage_managers, file_manager=fileManager, printer=printer, app_session_manager=appSessionManager,