From a98c6ee558f4c6b86fdfc895303fa1eab15d632d Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Mon, 20 Nov 2023 11:51:32 -0500 Subject: [PATCH 01/11] refactor(robot-server): Fix log.warn() deprecation warning (#14024) --- robot-server/robot_server/runs/run_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 88471787da3..f371cc8e67d 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -266,7 +266,7 @@ def get_state_summary(self, run_id: str) -> Optional[StateSummary]: else None ) except ValidationError as e: - log.warn(f"Error retrieving state summary for {run_id}: {e}") + log.warning(f"Error retrieving state summary for {run_id}: {e}") return None @lru_cache(maxsize=_CACHE_ENTRIES) From 9ffbf62a9f320de6d4880c585567dfa4354378a2 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 20 Nov 2023 12:46:39 -0500 Subject: [PATCH 02/11] refactor(app): make deck config section responsive (#14025) * refactor(app): make deck config section responsive --- .../DeviceDetailsDeckConfiguration/index.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index f0c119117cf..efb04233d0c 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' import { ALIGN_CENTER, @@ -12,7 +13,6 @@ import { Flex, JUSTIFY_SPACE_BETWEEN, Link, - SIZE_5, SPACING, TYPOGRAPHY, } from '@opentrons/components' @@ -162,7 +162,7 @@ export function DeviceDetailsDeckConfiguration({ {t('deck_configuration_is_not_available_when_robot_is_busy')} ) : null} - + @@ -213,7 +213,7 @@ export function DeviceDetailsDeckConfiguration({ backgroundColor={COLORS.fundamentalsBackground} gridGap={SPACING.spacing60} padding={SPACING.spacing8} - width={SIZE_5} + width="100%" css={TYPOGRAPHY.labelRegular} > {t('no_deck_fixtures')} @@ -226,3 +226,13 @@ export function DeviceDetailsDeckConfiguration({ ) } + +const DECK_CONFIG_SECTION_STYLE = css` + flex-direction: ${DIRECTION_ROW}; + grid-gap: ${SPACING.spacing40}; + @media screen and (max-width: 1024px) { + flex-direction: ${DIRECTION_COLUMN}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing32}; + } +` From d526537ff6e37355b66a7a8d8971e7623b16cd10 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:13:47 -0500 Subject: [PATCH 03/11] feat(protocol-designer): add tooltip to disabled modules (#13939) * feat(protocol-designer): add tooltip to disabled modules on file creation Certain combinations of staging area slots and modules will necessarily disable the addition of other modules. When these modules are disabled, their diagrams should have transparent backgrounds to show a gray enclosing flex, and hovering should render a tooltip describing why the module selection is disabled. closes RAUT-844 --- .../CreateFileWizard/EquipmentOption.tsx | 88 +++++++++++------- .../src/components/modules/ModuleDiagram.tsx | 4 +- .../heater_shaker_module_transparent.png | Bin 0 -> 21995 bytes .../modules/temp_deck_gen_2_transparent.png | Bin 0 -> 18731 bytes .../src/localization/en/tooltip.json | 1 + 5 files changed, 56 insertions(+), 37 deletions(-) create mode 100644 protocol-designer/src/images/modules/heater_shaker_module_transparent.png create mode 100644 protocol-designer/src/images/modules/temp_deck_gen_2_transparent.png diff --git a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx index 3d1bd093319..0acd1df9c28 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx @@ -10,7 +10,10 @@ import { COLORS, StyleProps, TYPOGRAPHY, + useHoverTooltip, + Tooltip, } from '@opentrons/components' +import { i18n } from '../../../localization' interface EquipmentOptionProps extends StyleProps { onClick: React.MouseEventHandler @@ -30,45 +33,60 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { disabled = false, ...styleProps } = props + + const [targetProps, tooltipProps] = useHoverTooltip() + return ( - - {showCheckbox ? ( - - ) : null} + <> - {image} + {showCheckbox ? ( + + ) : null} + + {image} + + + {text} + - - {text} - - + {disabled ? ( + + {i18n.t('tooltip.disabled_no_space_additional_items')} + + ) : null} + ) } diff --git a/protocol-designer/src/components/modules/ModuleDiagram.tsx b/protocol-designer/src/components/modules/ModuleDiagram.tsx index b822da731ba..7cdb2c584fe 100644 --- a/protocol-designer/src/components/modules/ModuleDiagram.tsx +++ b/protocol-designer/src/components/modules/ModuleDiagram.tsx @@ -36,14 +36,14 @@ const MODULE_IMG_BY_TYPE: ModuleImg = { }, [TEMPERATURE_MODULE_TYPE]: { [TEMPERATURE_MODULE_V1]: require('../../images/modules/tempdeck_gen1.png'), - [TEMPERATURE_MODULE_V2]: require('../../images/modules/tempdeck_gen2.png'), + [TEMPERATURE_MODULE_V2]: require('../../images/modules/temp_deck_gen_2_transparent.png'), }, [THERMOCYCLER_MODULE_TYPE]: { [THERMOCYCLER_MODULE_V1]: require('../../images/modules/thermocycler.jpg'), [THERMOCYCLER_MODULE_V2]: require('../../images/modules/thermocycler_gen2.png'), }, [HEATERSHAKER_MODULE_TYPE]: { - [HEATERSHAKER_MODULE_V1]: require('../../images/modules/heatershaker.png'), + [HEATERSHAKER_MODULE_V1]: require('../../images/modules/heater_shaker_module_transparent.png'), }, [MAGNETIC_BLOCK_TYPE]: { [MAGNETIC_BLOCK_V1]: require('../../images/modules/mag_block.png'), diff --git a/protocol-designer/src/images/modules/heater_shaker_module_transparent.png b/protocol-designer/src/images/modules/heater_shaker_module_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..349024bbcf7f1e71b28c9d3955a5b9658055d8f3 GIT binary patch literal 21995 zcmYJa1yozl6E+;&Aq1B~fIx9CP^35nE$;5_PNBFFHTqTpSu2 z+Su6W?d@%EZ~y1nvd{{DV=6SyrnAly|r zCcJ84VPR}+Y+_;p4hXjk*MYY^K0b!)mz0z&EiJ*T3JMC~UL_aX)YQP0 z;gTOee&punmY0{qdr? zuCAk_qpYl~qN1XurKPT}mY$vtuT@o5H8nMb3*i!Tb8|giT@4NOxVShzKEBDxNgf^^ zI0F+C<8TM8t*s{~C-U<0nwy))$H(BVHZ(Mdh={;>C@d`G;9&pw@grRG?CcBzp*ugn zn3~dU_u|e2|fm;pgXv>q|*VL7~u*;o*t#aR~{D!GQs7EiJfp0|NtiK;`A- z-@SX++tULN1l(=7EkQv+YHDgY=c1yb@BqDk{~q2K_!xjw4W|)KWKJrl$MrWT>MT>PIh&1MMXt}chk|q0nQAZsK>{r z&CM+zAD^S6BRKOG78WNbC;R&c@NsKvYYXS+>({TI9-hWV#`*c*R#sM`qod(t8_vn% z;u4%dYb)!uwe|SOz<>Y#UFQ04!Y4qUhnliF00jdZ9|uU}NkR?g5)W{Y;d|$7OF}{5 z_nr=omBmUp)`Z2$fS!xPhTKxf*GpfOpVm}CTU&y`^|P?4n$jl&Ep7!Sa`iMmne5=Y zQgQb%zW*ZgVbHG%m95f=zwpm?SqAQlf-dG=6B?;n1cud>#LfDCe*1P1VQqXof46*f zdfd6=+U;So*?;@{qV!;_J}2=0U~PD)VtH}&=S-&W&I16T29T2y*YH`p=nsZ_4G1cD zt01XB{9p0^U77D*bR8)okOVFpM#mb%BN&thZoNsV{gew63+?VXNm)_3WqQ@YP;{11 zi7PsD=el=ZtCkH8=GMz*FSjqiFXK0pKc98cX~KXK|MzxBl``z{(SPOhCm$P^;&Hcc z3ztVZWLjius&v>X6HgA*55JltBW}xCYY@;A9bn9pN2V8r;*TQ-34oc#;}WmVTpt@i zPlYYjsBl8E-s9t(q=pb86Yz;5G_iPNS_|O zJ``7Mm)c>bM&4k*kyK*o}wR^UJZ5hi{k^ znYXpplOp}<6;|sZ6&7d9KYQ7s72T*mT@?Me<~@PpedUvfH5hwKJDe38HC_S?BIr7r zLja<^hvO?H;H5!4Mqsvw(_|&BqB`x#GDj}f*1=9{YcKK7s?GSw4v*q&$uNRK<*wZ7w3()}u^eJlS7$p-3 zxQ>bAl+K!hJjwSoMN#L45CW}lSSZLBKV)qDPY3kVWMr>cRTV?k)EW#9P?m@03b4c; znH24EKDVKHo91Xq$5G%hNlmy-3bZe@$SBzs)aa-AcWIvXF<$3OwLjWfx~~w*s>`v_ zrM?Z$Pt1kM)^PRfr?JPqKOdyIPMb8vRv#!~YuC^YN=N&W8Rk1X{-iimYPmG4~#c0eoB>E}l0!I(1g^uUf1&D@k03fWt z)g9LHkBDC)0)r|SXVWX$6!9hc5HT=-W|2Cy^z}C=<|H|hvGw9AoLnP#HZ-Rthis_w zXc4jq;--AL?w8%|%JUMFkV3Y%!;b}pjJ^N;{P?goGuw7F6RF;c@7MO2BP+6lc7@K! zq@I8wwEl&X(VT!_Sqq~6uo3W*`grX9DTeQ4q*IQ>^(0FlGiVxQm5FbH7$I&n z^sJ9ztGB4PD5vG42p~%dDjAl7Qx&1@cl+jK<`QYK4{+ zTl5P`^ceGAN^q>CAm}j10OL!68oCN0 zVT#`Rq(RsSd!<2;qP!T_?kcW{W{ zXjo(qBRFGd)|<&-cva?7z0S;3EEm;hr5-tcTr&?>^D5B2d1K#9dC_r*<>v+n?8~EeRmg}zbt+6e5DBffE-tWsooFa-8 z1_I*dd%A4Z$XiQ0P8uYYb~oMjx_*7t9nf9I-}vZp*uS@hW&x3N&OW9l@U(zS&*#Qg zGhg;{6(W}D64-)05xH>(MT>RlyYx_Gqjn&0Nc(zr2jK>i#t3Fe1;t<}8vYyf;zkhJ zz<>9_x_TGWq(b#K{=B?Faxh-Ch8!J%L_=6}B^#;?uOtucdID_}#t#F#BRgF!9P+Lx zjE}#)LCYcz@00(njx(#bZYPCec91At>`EVDhIkoc(dajrwd1TSobsT|4_P@@WrgCl z*)tdq@u15XhyKuXMfs6?Q7rX9%iyx|@5qA4ATpMCLOvFUSaMN5bQbocBETr+8N9+v zLz_qZ&Y!`e$ON^(w(N72Pq{&flpsEjcH#FTm%`cqJ_;;&W_N_8hX2}DERPi8HUG=b zSE)sy=i=C&NRFY(>p&+?b2f#VX_!Z>!Hzow;jVBKKec-p8ses4rb8PVx)?h6J5gaJ z_r(~)JPvcQ5KO~TYxZHbn3G^I{Z(au=+|0J?)M>1hqd#8(rAsMqa8actIB?L{?qvA z_c`MSr}5~Hs;XH7Haa%@*Aixw{Rd5*m;rS75);bg_l(XhS#oM|mlO19gkAlu7h+7V z;UK>@$a~(b%=XL5T=^Z&lxlTcCsL|-J)=>iKxp;=H+}4VCf2xLZWy0^Go_>lK}__K z2S34_5^H-%Mj|f?(dk-Pvr)+Ut@duRs)QJR^EqF8RvK6!2czbcpnb){&6 z>BVmF`edQNFsJPF7m>q#X2P4QM1vFc7ll^*XPtIzL>3apIgP`!7&$^l8jbe(wia)$ zpbV%%wr6D+s~X<1;96xQwPDm^#OEYCs=pC}KgZ=!J}&IL<^PE42}nTRB6T1->4)MA4}1@EwOgf zs+7`VchahQ`$Lm&uzq}cvZ-+Ru(CzsmA!X28E&Be5oK!IcO61-Ffg)6RxcnhM2!32 zd??5xnTDGtEZ4$(Gt8>UGj)YQd;X%4Bjd64$9J{9KM_44d6UX-qWqU2bH|3W$SU#1I+QlNCfX(H>e?$9x8rGlIx5Jb^jG- zvTdpJ*qtp^qwU1drD4-(chGEiiR}3TW_eGO>~9dws=FnUX6jy3ee*OhU+_9G-rA+e zm0cb*9Y~$E%VX0V;t)(+e_&}aJ z3ub!7ew)IBGV@ORUmBReWft$D9Q|S!$-;MA|1&?6i(Jk7BDb_ss7*6^T9u~FW|g$V zwV_1W25vB^e+ILpa`pg=)`{ZsC&uP4tJi%O< zX7e9h5Gvise$P)y;{-kp9?MHyYSqWca^JJ>)>~7jM%kht69zn=hnDAXsO`qzz@fBWY4LbhjSQWs9pX zi5jgoU2G)GX1YDt$%hTw0Uwy?!^uO{Ke;ls0b}IUY7NS+gh=vp!~Q@=<(D65S-PyI zH6tbo|A@r11l#!Z2;lv>mLDWrOd+lulmy`38lKTZRW4c+J+JtcpMq2Du-^F za%ojvIvV(@3Ivq~h$C59Ioit)d)w%sccg!BDkz#PzT4UdQTq)t=B+G&0{s%6+`86!|65K5&3OT`phibHp-h@__ymEfJGENKk7j#t;P?n z=PqZv%;ckSm39nz5atZ-q9uk64AGVqt1`LoBHRey8W>rYvH6m0R&uVE>)=0`mW~wC zf1hVBRasd1?d?!~Lo%y(%`I|O;kkKkrP@@}u*siftjn@N-Y@6b`T2fU>arF)C)~{6 z|E1AUMQyoBl8J1P>aqZvZ9YM_aLriG7vV=L(8;$wiYEi%uUND?;dO+n-a&XQ-r$bV zYkbO|Is2UTd`Ac8AHC19z4^)R2GKDio2Zkn9Us1k@Fq6Kf15ZYOiO=W~cD!sC1lBp3kfge$iM+Bmr)qe(ioh8lnJ^9#sH?fP#MLH_7Tuxpx9$Og>y zR(bkfJW5e6CH1i(tGpCr9JVuooZ5tm#5p}S8ZR3+US7LNCUjit`iH;JV!s9{bykpa z{=(8sqlBO+;p_O>nlGVwUHo*3LHkC}Kq-ZuMG@}nmYar|$|3pq+<-0dskp=2-wlS( z#R)Esk@N2?mr=FW(CRR+8Bx&izCe9>yFZn=m(xUrMox1MR~IKvlTc|l`iwG z6xECwwTpOK6`Bz=z1ZKFGq^rt(z(YKCO7RGe2HdR^&v|GbjzNV2j+s1JB0>uO3}ACpKrZO+vSnQa+-Fkj_7@T zJ+n3#w->!l%?yyQ9@wRzA3UWt{WWoeWwN}q)F)@58=89njn>N=BhZ|UYG}ah!*f2_ zp^J_Vb5o;U{D_MccO8lsXWY9H;3wJiP+6n8+u)xDurU{qh)Z9b*od3K^)$574Rj1* zq(|YE6DHA=r6&Ugky7E#?xLE2ORlULKMX?BAz#Zg;)zzK}@C8$B+`&;<}^n`-_;wS_*NO#j-> z5bE*WY6=Qol)kT$Y*K1KII$_QFsDZW=erIB@b1;&7U1}-cx%Y?zixaa=*4C2^D=zd zuR8wp-@Rk4#aqo=nn3x%2`t^M97?6lATln>>>Qw0DSM;hSLx*<%pRJH3=VY=D}|^a zq03!YB%C1rG%Amas32qf9vH>bw)w^St?K*NHv)AD$!l+oHeBWLx#rgt-b;mWrT^N? zYq`ZRmJIx8^plS04z;0aIktGzm7J0g6!WiVeG*W5elk9p}wv)9E|1@y_l7Csi) zGExLcl(7t2CDaJR*5I6eMT-{c9p=iRDD``CoY|I`7?HE;4;O zahaR-0n#!G9pGOeYlt6aemSo4*fk!R@^KlCgji2R)b`T`4NZx~{w+nPKSw|W4HHwp z2F=zkVQ~^sW%rD8w_Za3f?xX9QJGg!=r!8DJ@PQfX&fB}Xe6P`XH75I0TiL>A-OhK6CkNPPq$W->}G(`hytNl_exl^6uNdq3c4 zAcmj+&M$VNU-3?;NlvHEaOiUg-|mbf7M9Sa#G`3FCdc<>rXN$+sRn=$gFsoyHSt(N zWhx<}TnQV${Ud59UGrc(*t@vs2gIMj2+zyvN4@)QtlM<4x=a4rA4bDnRv@xa; zMwR7uZ%d^WAlO`HxEm5=@l?p znQn`#23xT}NL5%tVsf%~YFHrs)x{5~eSW1Ejx-&`&y5N3G9W)HnL!9`-OZJ+3*j+B z9tV9GX8^0g`11KVg$okWby?2jkIUaZEUB+OlWCa+QUXsL5AWD&jP#s#Q1`?ffOvSl z25I4>5u9e(j~|F`Mssq`(M1Rj!YOMWm{fniE7Y#A@GxVg8?s@fNX2mn@?9ABSfl_G z@#{1^jzgm^MvimdO=vN4x%A#8tKE*)lHH^}a?_LV%U36z+DOaw;!H%SwurCO$qw27 z{vz<-R;RP@_1Jw{p`--1QkW#b9oq3gCM(Y*B+e!xkF8;r3`!#?M83K%SUyux`=u4z z`++~UJ)Tj5gw)VbFo?hOZBR1n=9cWH5RSvj)P!>VB!lDZ#rg{$f)?AnuxaL6lW!)+g1YIh>% z(s1to6Qj`X*?4oaK0fBCHF))Xjc@(hd%ZH9M#F<5QG$CJ0`>0s_uq>`274h9y=6C= z+O2vo2<5L&Z3%T@IP052^~QMvelpG(MZ}yXlA(tHRHA!yWF|w%*x>bMB8{cj;LE?m zZ4U5*Rs;sdQ*68YOLSh$K$~~*h?ElV&0n)F`W_zz#kh>j%#+w?;inT((;}|9A+&fI z>aI^_`hqH~Q|QEY{$lu<+#aanxahJXv;(T`jOdgCKAa=XOI!Y0-$i(^dG2qOzBhhn z%zoR~xBs!}#V;uajEv=VSVj$x?R~v{xk8r~kOs<$J^9_I)YROlBaUBwGtWc9Sd#)* zj7U2`9CmBj$twx1$q6eKz;sqP{o*OkM7;vpnxMI5?UtaC{!_lRmZ4z4AYV-!yf{9;_rbt_D9p{7z@ z^^I*!$VqlMF08V)uZ=XZjn9o`7y{_1EUeAmaWi=z8#`GXB$8qy4b$%B=#%N);NB19 zeE$WTpb#!}nnhILj(y+VuO`OmvW8PgUCh6mAo2Tg z;Us8;IzmGkrC?otTcTAgMm~qb3Ezlf|9elT-S4yXKL78XH*GRc+ISd@wN8l^nvVKa z6G~}dlAHv_QK^t4@9>&oGQiTLR~v2{`pCqn2XVgeaJ;08SFLDgBs5ZF2{3ed`NPpY zx4u3%_qF4LF)Ud{!urcathoracZ+9%g4ZNedDsCY%}p%cVAhuIF^)cGLp)Y}sja-5 zevT8%u!CFLZ`N8KWl0+^T~PnWboto`1&5Yfp5yWLILP33%AF(rFJi(s=jFJrE7IW9 zAkcG)Xn^S7kKj`$ioEql$+j@req;#3VQYU4TlfK9xD?XQ4_FpUtYOg@_s_PZiwP=G zEgciQdftC&oa4B0YCF@MjWl+ZspKYakCeO_2Z2qjPD#@ zfTF{YNRUixaM^3(G->CW1cR^l#?p`)e6ef}X5o7}hQ*d6$Kag%yNt{bv*_bxQ^&D;2-aA0LofWN2fH3;0i{&BD^Z zi?WS!GR8ywuL>!FZb#~3^LxVhBygNExjZ?szoZZ|4wb!bO(uWTh^(Q4xgs5gbZ|TO zVUsI=3C?44cb;`?V?x?+C-pnySw`YvcSUqL9fQ@i?U&t0Z_)T-g=ZRZzIHW&)ErTid$BpgnJwcLE(209g4Q zds%xjJ6U*IaL6f>qyOt<^U|;0z_;nMW#iTbgfQPssZa~+w#V4?J<>`SqxNxV=`E_T z-R@x8)q1c@K!-^8$K+!pN@?dW&w~kv0}$35MoXo2yaCu!)hK=Q4OfD6lY*1GuPcS& z{O#`yU|WyCU$TxcwEp_rye0(iR>5{#6)tnDPtX755Pa`>n&B3xSgw^kVjGRZmgzP~ zjtEDzQ}Qd0?MC432$tFGpl736?p@j3T)KJsFETxi4FQ|*ixu1Y_ZP!pblNOI-`j&b z6?CMj`%p^>iC~2T5G&DP80H!zNnxgsye2)LS^}b8nD^H8r}f8Y*&tG$<|^#o0@oX> z#~||<@8%lJ*R0p8d$He|5`L^DpJ1N1v{Kd^;6raati8N3;P0p{Iyrzzy;Q z-oV$J(NkM=-Ts;9XYo~f+KeyHt787^jM`UW5VI5>>Y1stmSX(m!al6KtgD_@vM6a7~#tgNX5Q``a zMm;^?)uN@otmXo_jAvMnqKQ7;HU8&CitG$i0EO;|c9;Zw&W#YyP$a?@#hz-$=*1qY z#3|W1tl$K?qej>>JGTkjmVc0>qpI+eO$k}6CQjt9Rl*pv|N3^nd=pd~I_RM+07Msr zpmVt&l%d3@TrbBPqW?_SL4xE=PG)XB=mkFhz0*BSPB01%b3l|LOcC`msf}h}#t{qo zS@UPGE9>EW^7KzX5GY0-&Y8hNo^HrF!9$n!-i+cNb4yzC^)k+m%}(XBzU6}*;B zn6+x{!bbj}M{5fRfBz}gKF9yT5c>-#?yvBdjf{A6W>^k+JRVBC(dvHpW3oO+Jxt)F z$_5|jexHOx5o2u7MHi9Xnl35MVvHIQ9oIh?%@&haBrTHD{4^@eAOxyq0%SF~9eyu> zE|YN`rErkEdB3z27cMnMZv=(j`u2J$=V!UpKZXwR9HM-gEF~sH#u>HYj&;9Z?sy}Z zWlSk(rKQCGck?pi7v_&p-C&|PNIt}IuZHCm~R}elJZM+!xajx=1k52qeS~pK8Mipp*5d!(4S{Gxw&y8wwu+Im1$wdlI9_~MLdl_PM^xesN zg}7oJfT&WIj3^DWSZl$sObsF+YvRqvy|5uv$ldFX&CZ@$y{y(%*g>Li4Uo{FkyU{J zdix3Ut82l}!B@<0Pbr+`gb(pCFH_is5w@v+P%Uo)OKy6R+>pYm?-gZOY+ArY`g9-+ zE6-1#*+Bla(D#+)?CMY{zwLvw01)hpTeWq8e%^@)kN9~rDV-~XN-O)xwqBlc%6!BY z;01G3AlKvJ^|3;>RoQwpMht$sLE6OO{zEW8$DC#p5o)`M6kDE2AGA_S{aD$^n)git zIxHkxyKkJV5aBu$&i+yT_T8E6D0kXUqL?0pG@Aheq5a7#qf)Ez&x;^w;?c&{9)phP zAU>-$AN8zs4&UBIsCI#Uvg2a6ZkE$aTa#^+u47YCmGXB3$b z?He!%NE$xjo$EzNz?}EHJIJC|qi)X@cU+TfN&eK7)ulVA*lY)0qdXE@5&xlIT>CCX$t=F?EY4u-6k{tRIGD!kqLn?=Y z7?_8MfN`teAz~80{K*0ZAv%@>u-PXNBa!G8*`-71-8VL|DOIQ95FeLKvKdoPppB}(CztRg(UwD8*d6NLQ)=?U6pyFGwGf0S2uZHK6;!niW;_j zXAPdvFu$;N z5Y+W=QJc+a#9Q7}XCekO$;E;(Ei+6iv`b;KfMilk{*n1qCdk}GA3TSFfxoI7Dx|CB zvWCyG|7@YflZoCLhQf|;6DC+m<_y5$D$Cwnl_Wm^%#6NkQjo-kupz*xFRI^(J{f-_nm~QuEg%fXopCm7;AaW zF7IBdb)k}CFtX!7uma6$S;N7FgE-P~5-As?ti+U8&iqDffkLbpX~>-J2-Y#*>2h;V z@LpLmsn$M%0)TOf2fK?JyHrUBf=JWWYNJVnbCH!$h#3ebIiwJ^twLa97s?d}>$<+& zp^j!OkXl4;wMhQgY+i;aR9_^UFUq{gu|6Hj#;s)-2i2h>c%|gW9&XWVrzfEHXRm_Q z+VG6KTnG-43AQ3s!)x@3&;!HQ;+fDWp$r5Gh8Sz*lna5#4smGl=}^ zU(4fnhbD1_4>mvUEV9K$-bsfHai(f`y@#OA%m{H3Z1Bfhd%5={Fa-`hOhQ@35E*Ps znTT>fuQKmqh*}Lmc>y5gyI)qci&M8+s}$6i0bI+B=$H0pV$LDpp5OzLMUD6bSv92F zrD%dsR7>1?CTacLOJch79G1Y3(l^u+3wn{SOmW8NoB^JV)sTZGIW zjBD)?Kjyk3vpqEk`X~LqIe@cVn8jlOpr_&yJ6BETFUY*8=ilc>aGv)wI$KaZ)w9K( zn;lkh@EI^ny)xMAJu8oHV!~;HAt}SL?KW@9WNihkg)zvAW)tgVASkv6B_+{+(LQ~n zc>i5$7t_eLaYn6B3uW_LfXyU%OVQ?$=WB;02w9?~th}eEO$-xxjJAN>H(*x)9f_Y# z`_RH7y`&WkO*#pfA`j-);)=@dpUfn3Z?iv%U+V7XX|{o3Fw5;E5qj#|iEVvyEn3dd zL@Q0o*71lAQ|3=%QuH3eNQy#=MowB;`JN#7haCprc{4#2k?DPBRZ;upDenTfifg+N zG*7m6WQ2+55_%Qz7>VybXm$j-K3le16NzTWjf- zhEA28hKe>Ch_%6_5+;4K+oFi}ZZvtehF#LVe*xil8zc;5Wv{tep4oKdrbi?Cd zebp+r1m@)@7T9Q{)6bxnf|#5tc&_?}>Xs9)_S0ZeVM#zdFZ6yB&RhG#tQGd_k^dej zxJ7rqVI5g9*5Q>Ws!A1o>P}^bX%-9OfW!;&JPExoKvEzALy4>L^owk z(4*qEC08KB2oXIn(nE-+^$!Pqc4802LA^Q%#UNl`aGRBHpE@=z*$ zoySFG*w|+rlyuwZMjuN?3sO_y8hZ4^Wmzt;_+5b7`p8(=JC2lak_H7cu~bFbkguOo4G_ibEn$4FQ39k*L zJQoihQ$Lmcb{(h4bq93-J=VLSPmWPK)bkVnoOw4)`g^0gpdX@!>K42+*QHR40M?3& zbWhfpEf@u(1SycBJ3=K_hczmORFK$}uQgb#ShukX{z#|Z%7oD7NFfA^{F;p#S^Jyb zbw^;M7`Wkoa8bZ%@LBvKluto65wSccKHLg{b`b zosSYS^SN^DMgpGlKMqWNhBtuVamOt6k9fj+9RNs-)D5G4gjzH?(r9UCw)Wk#Fd-YR zxOW9U=ngtXBV!d1?)+W%(|@|o?~<6`QFypl>Hqx*r7VyHudz_c|Nd#{CZ`8w59;K8 zUnH=@bQMkm1kR;LmYJ;jA>BqH4wzA9O=y6qT`U!~Cm(iy1z?~X@Cqs+u0)j}MkS$; zuOHxKs)@$QbfSOGZ931muy*oxcjhlRX76Ts9C^kuBE7`;F_Jfk1`EJ3kJy?(4O;wz4AWeRF84X~FHMnNq=vnNjwMB?(M2 zb{*`|5C#2)N@L-n&q62`pejp7yo5U8G#IR|#RqQ2-3QFGL%;<`!G`3)?QO*5Y&ppU#r(-72!jHCM@}tG z{|$aGj)cUWsn{j$^jd12i3N?}JQ%p+fZK^-CSGVCQa6qN-7gc}U$d{sCqt8h4$>ps zjDlr!{oaa%@(&ie+PFIloT!O%G1j*cWZU0U*>j3ztxfCcqS&(WPe}Jr|Xi zT{TWC9>RmzZEp+Mz6~`hil^SAxJ)L8BDTN$&4!LK+#{_v9%hNX!o+SDoZtWPkc~XZoauJ)y^siN3IXcRIF-N;M-d|c^^q|sTeJX=n`IE*BlelW;3S4Q&IqkV-+ z+e(+@KOx0lN$T=$oQCZJX!x^1lCv6lfcklE*fP59&~Q@(6HDL-$8dvpx!IdlIn+_L z_X8t)A%A5E^zT*sg6M34QTO>}XSiIgEM%EC^q|IA`>RW;xJnW4MGpA@f?cgRCH#9| zfV>7XxY?9czznB+z8^0?-ivaPEE>Tv9zrAdFZb-~iaH3#dm+wFEKmfokmOs(d2MID z4|bBlXV>%WNsYoW=ov~whv4ZLtkhmIg48Ddz-4o?f+jRYb{}d}2m&KwZp;f$r7KN2 z)5a$x*y##mNL=DZ24g3)mOHmUv}?9r^FyOl`SJUN@)2o23*Ur|LFOSDNpi@^bO2v} z&-?I&}5oMdRsFNb5 z!H1N9*OLHk!*D7D2y+TdjtUKOINu!zHh#2YC;^C#Jp5^&wVlOd`09cM6IeRqaR0s8 zEmkaFQi&6qHbqJ#=pDIO3eRjvjFhfom#W^Cz3ROB1i1y{x9{VE-QIBc%ZtLI7Vr*Q z7tIf650GHrE`x{7X54cywPT{r+dK+fUVr41cp=Cd_QworA|rR(eUJ0;60mN~peRXP zOc-)3Q;#(2p$;mLt4(cxOVD@fkp}v{3KF#>01g^4!TGozEnnx?mPz6k&g0J_FTht6 zE7uAAi2c1h@sD+9DwNTjL@W%MNKWITfj6p>`TbQJiZOwsE}CFGI(&cxm3Ybqu2e0F zsRDk*!`{}m78+(pDk=-ZS7jdbZR_V4lyk;1l^mZz+=Ca+e@^0bl&?oWdS^8D@wN91 z=G$dj3?_yL)xQo9L~o5gSoak~oQz^ewFiO3sQeyH0TNPFUFWu#`UgoAv{GlNxBB{o zV+$lmZ~Ob!k?Rbb2C7xNNVrPluck40EOm`4JDGxjkFI;?36i#)iHtFqq;WIM;>=EQ zkjS;O)53rc!q^N{+eU#Wg^>Vs{Px@a&Y&^##7);f)BK^bu&61zd!$n~Brd?1&c6)v z{BNH=`BeK4PujX7HnW*!o1qPS=C7MF_=HsBaxpX@@y4s8sUA_;uu7gL2D)L~dYSzv z()Oc*lbEn2v$ll5 zUt}3%qmGn0s3o&eLBi=Im|<*ij>|u8T@VMt=xyLX?j)FxfEF0 z$s>&@JRt!td;l@oGPNR66FWBGLyv+fO*3mI`Xt;LM*yF32J>0ruM|<2Tc-nn<@<%` z=yG9Eb+(pR2WR<#YdMH!_jmUf=UdAQvvu8`F54|N6#nw0Xr)N!VJVWXD!eL8&dlm zebPt9B-TU(1X%tyiyOd}dfjN-z(5CXNTc!l4-2}+d=@DbWiGjMex zwz8&GiK$jZUq2&vOc8shykx1`T)E$KO&WHXPmty23`iAAKg)EdWxQW|u={(Cg$5zY zPL)KmIZoQNP|vpU(uTnwNj%3*?xcl(|L!8Euyu1DKWl1vr~@v?S%SV))f{Cnn3D2M zeM&PcEcMVsO>wcY%MOb{qzjMW%{KD=ax+11q078b$ZiF$hOzpXOuv$yxjz-s!EWl8 zI-V1Io?M;H-&zb!bh%5?&EIz zqf*{KR{!|yw9#PQ)V`(axi^X;BWR$hKIrb8*W`034;Vlc2cHrSh)tdK2_Aewp5Gy$ zW1X zG+6P%-qGQeG-x5!iQ)t+YAd=iWa2_?%lI**Sz4=ZZ0mcF%^qjMemSm$X#oO#fE|e3 zlNAfqwEU*d62-hn>4w>PQBrd*rLv;X8+duOx*nK}9P$jNQt1$r+V=O+62KL?qeQ%w z#IP+O4vLGr*V4Fryg0v_h+*;ii;@dm(0?4%Fxs7cBB-kt2NdB@rNmR(s%-$t8Az?9 zUFOgX>FxYytx-Zvh31Vv=pw{Bb+uYIA|ob9i*PVR5u)GeRF3b}K3rq-qL1!IQrD*j zy`nB5YhoD-EQ$)~sA@jm^NW&XI8h*ia|yq;3B3H~LNq%{DjTig8WotgSqP|{!e5uy=4 zdeOp(K7`vM$Qfll==J#6xfiPQvJrQtz*xp%(2zdxM;CT_}&Hd<)<6X+1oSp-F3r_7~WN zlYaK_2OxQdhy zlh)A@d2DVq=3U_DBf`EUr~xGhpY8=X>uC^=)|c z7{3z~n^P@FY<{1UPitL6iJUTvdk|}|5Z7C$uTj^w*c)i$fkCb!MTem!y$7azZ`}0d z(~VBA)o)BeyMPFRxZ%icGBhCP36e{0)ET%jBqfo=xa+SW@_d+gcU03(*y+J3hj2ma4&IplB3V$pFtz&jSC1kCTcN)18h)7OHMRyK3f11^I*A%%${ zbaP7dHXbzbLcyppp)R>!=2ri3=%(d#22P_M(BO`?a2?T0sDD9Vf zTE2>Yd2cNYtY2RXxxq38^N=fc)g=?9z`#7T9V^Sz?|>(rUUKsGaIGIMQnEtz&z zcd=lJN9Dm5FRl_DuNUXVkX)(47AOc4wh9vNt5K$- z(=!z61mQVoRm@wX}HiOH(>lQ!?@SsJ{u=&Uq|pZK|o-P z3{Au{n3Cqeb?Wqcp8L!bkgCu8_f*y>_19I)#Q!9kdvCfqX)#j8bZH3XWUD#&FaSKX zWWuZ&C$kURbTBm^Qkw;vkKalKrdXpQn7DH%3PMPoKNC|5xR{ujs0a|Dq5Y58DS@$? z8DZ1@koIGPoKba%#f1N!M02{p5SEGAPZ^M z4uasedsCfHsbY60*KrhC{>)l3sQGGI;k>wUl+a6eJ)$`iS5{fdWu$~oeglLvf!&pM z_l%gT1f!Cs@0@Yhu)WxO{J~C~+i6hpdR+BowHaLvz#`?L?(>nCYoq5wZ2o9Z3m8&13~CF0>9(_{Om>DH!lN?X#y<934UIbK9~An zJrM^qn*&CM_y!O-ROs(a*k8Az=|~511a&Ws(J;_C&L`%Fc2^O3A(2wtx)Buq`rbBC zJ3+(vK(xhcD#A!7&50_Ncc4*>f&}+k5ss^qVh+XKAZ`d+h6gJ>j3;RHH_4!UJz|Kk z7W`eYlURT0X0!pWYc|OI9#(i#=p(*jrlOy3n9ym&hn@nIf8lF{Fa+e}z*|l!UsO*; z!51$4_z1?bV68Mt(E__;F@qRMFHKh5kzEbhtvOUrQWm1qR4@dn5{2m23~2DVL3uCI z`=VXB`KG$7-;627CTy`7W4tT-V`1ROb&iQO-qvd&n+8>^UI`Dz!Fvv%BQw7=n$}^7 zQf&@$2G?NLhz_M~IXU^(Vm_nKwdn_dM4kfgSXFtwndcOW=YgCj<%GZxqti&g7?j4E zRp8$Ug>$drEtnP^*=M8WB*EJ2Uk62n#;aJ*VO5Rh_Hvx!3C`Y*1nzB~OAEk|Sx`arb=?-M)mlbluJ5IQKlYkdBsW+U~jR z3E)lH{dH(tTH4hV#A&oNjD%Znom3o^01uh(!v8bxzCUoPG}h;*Nv;!BVKEs4k?(4U z(^*=~eM@j@V_Og>YIqAnCgPqFi`y;i)Er^Zp2Q)*R(8e3)%gg|3L6UcxdPAny!7Zh zbrYDBM~7CQ8I%MEq}^7s%qUjC=gGjs7%ec%m2#+%oe)tBhDLk?29M@7q5I@|F5~tKL#x6=51LYRcG~_0TE}!&#|*>QjNS0Y&#D4(%9acgVkJ_H z3qLe4B$C+K6aeji7Oi*X#ey1&o$#ci^LzG!{!akp6dUU~c;AYUp$Y_cZ3%T&Zy7C- zM9P6@0D_gjuS)x2oQPrvC_gqgi<_iL6SfRizho652g``>7O^t!+q9YxSCYLyxGSFZc4y%d-3q=EZTpxa>>wvA}Rou-1YNm7d6OBqSt) zrbR%0-mJG*+PMY5q4bey=3p9`a1f0#+;U=V;QU9M9QV7#;q0nB$$>cLIHSXq!n1P2#7>z1VnPnQ_q$OCP`|65eZk?SSk9VCm5=>YyAQQ~Hz0r{lQE)M~@XdwLIr)?ojk!tiT z-$n$YR!bO%skh3Iev0RShWuM0z?R zegyKJ0>Y!YVG>Lq<+9w`Xx8`rwBXRK>LIzw!8Ba+I0QMjRn8zk^e;w-IY*R6R;*pK z{<7SwfXMR2fP{&FNPq|-dFtt{h@`A=Ot!#GgmseQva!#G_6kBI7>(_*%0tSt{O`fx zugz(@UEQqAJkmG0KttDtx5s6xN(BdtgOdYkn8lIfijFBEqIRnAoCBNz0wU`{BBT-J zS_>D>a*E`U7b>^nvGn57af?<{Bofw$?1lr>Wkmu(KkRJCYe5^nVEJ&EV!he{MA{XfwaT33TAQ18sY(kD6bHw+ zu&O(=vMLsb403LBbEXOgYb3Q4567&J6-3so&&>rQ^C8#)i6BBqQX;wUfv0OoC555| zwtbAmEtW08AlOwf5RCa^J;TuudKNamzRwW|%fn<>48x3rE1fua;_&pi&fv(`bDU^s z;^4k&Dy+&pBAx@?*#-9=zIM|5=izu^r^!b6w4-z35vT3~^sc>AZuC>aQag3x7!eZG#3 zmYCIpU|B8u_ahLUhbha=*&$hYa4-#yaX!f78UR@w?i{~X(2z3+i{s$G6Nk)p63J|` zt+~e##`%=jE6F?-?ZAl{3 zZRN`(Vc()C*;WBSKqf;GjFPB^ROL$7&(?1a{QkXhi+RrK!ikcG>YWd6G$Y>Tq4O=B2jC=AQEhW=?1o?WmO-F4Mi`K zQM&tYTj!?FBlHzu)$ork>lXl`O@mY>dFY*$PD5@0I94ky#$h!!3>^PaK{rIVPJqHW-qFY5e^l z%-t4>WR4OEB3b61NmwM@Z%N;RDr?>%`>g<%6)5^)=LZ3+x}(jG2H{L80r>@hH0t`u z8V8l@kXth0qQPBNI&oZ*RvdY0#qm$7L$|F%AxWqK1|ooD?FLAa%Yn$*XUh)wHZ>+H z-=Zjas1PDAB#J^FgB$e#B5jkGKn|IIXIkMl_AI7$+5z?Sapb`k@IiT5K)36p$3>|k_`~< zt1iDp67l*LFFC^Kjw&gw+ZE7Wl17d7|!-W z#c*Msfb2yeX1`*$(s8&>M<)~wUO5yrSR8tCn3qRb9Mgb?n%r}QNC3%sOXnhzGp_*O zf)1FFTm(pPPLv|y7MMo&M$*XE;sU=drV{oo+m85E&Ibh$jmIQz<=F?s9v5lo1E9mI z^0nkJ?*WoHa-gI*rUwyiaK{c9`Yq~!mnq*m8AOsVR6-(gw!qI(3tU=SGwMU%qA1bI zjt~%?hbp5yQo#{V@Whd_s#~R1=>s|8FpI-wRnx+&Tq033$?Vx2?pMg61lle3ts@;# z0z^~`T!ShpuG(eVP>l8Mf@`xjYxB7wJv?qDR}N7dREmbQs#!>banNcN(_kEG50I@Y zXZq0~9w-tHcSJ%Wq2(<=a*2!*DUu^koyF^0goH--t%zh$MoGYpB-}UfYYK=52lIeu zL$iiM_P9Vp9aUFXD=VuK2i@`w;?UcwX_w=Zhy=qOSLMmKXmY10kw`A!C~+a7(H%7c zH6F0-Hk?XahBXYAWx0T)kMlI*kinr_)m>+;yZL6o;XMUvKFq^7h{m+ZaRU-waHMb- z{T4*Y-RlVn5^;|1Ad`F6o#onfr#;@X#+^~kx9ss>p#TW$L-Q*Qh_ou&0Mu`+0OFXX zwN+Y|tDjeJC&x5qBHJzMfNARrT4408Ggquxy&6Y%i3EtK9bVM}^R7Fpq^z)Ei)w)l zKseA@9*jd>VS{&aqifDw69-t;U9?u^5r=aGyxS-aijIGO9?u#HC&>&olt7eV3#?5g z+G+|N;1VJ!OcaS0SVf6FK^ORLHVH%vb7fWP?nqFw#|5jR9LEA0`EGIKy7y$Y)hd^QTb;O5J5|Kc{6LfKJp%x%45C;xrRoyHO z4i2YP0gdB4R>eV%IP@T&zBz6%Eedz(TZ9C{okT+0iHJmHNj{}JGVw$S5CM_YixxO& zNg%)OW`!^hk6%e~5DkfgcgNAH?zs8pS=8mCIDm!`2d%8uugY9NQ_MAdJt1>Z_dCAKPwI<;<_aus07xxsNVwLQip|H+wOGA zk&(awE(((3!U}4EVN+ye2LoXqu67s2(FNL&H^+6|k>?I2jzvfVB8^THz`%r%aKi;HFiE8NBhdoK6c7Us5(lUOXb=zXs&2aB zF2DiWn6>a!Fi38zmUCOhI2^lxGn7~H)lUC~RyUhH_t`U~ZyifpB~B!~@9sqcqkAa; zfr~~HCC~y7?LZ=%T>{}e2XWwSF1q5~#3Oar$y=p3KpRpV=b|{~QH~3*TJ7Ei^j==` zUycs*4ljf|ZcgZAhSB(hmWx+_0O94HBz0tp9M z6>8%SvZ{p(k6d({u&Q~E;LLZeSLL};1cOXJHPGQTEK$<}uiI+x4`-zCCL{}25inRgafE*_p^Ddgl;z)Vb zrBWQqt2{Vzrp-3s)G^`h#2Gjzl5aUKypl+MXYcMon~I|-fQv*!bSVS{KPn=n7E?(Z z3qef%B4~rPq$sqC6gs+ga1j(79a<0+1Se+)b#`zP7gsk&SHYz?dd|7;_V!PWR&6CF z?}!Robog=a{lAyKcWWfcOh&U$6xF!Uw+O-t`RElw4h-@S3(p=ue)9C;!>`{~S663e zS7#TX!MOnoXq;51Dx>muQ6Q(jQSKqx|v_n#MT-C(XTH& zJ$>z3>(V8TaKkt$j50B(f#BeP+Q4h?SsZgy)~Y%@LV)7vp@Y>X`j$RV%%*#gRPlO_ zAjt;2UA&vg%DqEoVu1j zetCK4E+yOorQwZg#x0;Z0*xpQaj;X}ys5#V;*f@3mE^^-vww;s86_$bf6l#rSUX6p zN?_7p=b{NB6O{=%*HNLYEU&aAv1Z|oiWIOQ1u<$hV1x!bmBzRx<)1 z|9C`^h@^-Yrf6$Ykm%i8G?{P{rd(F%5(+~KSy*Kl#)#)g?F=wnqe=~QDz50zYww4> zRr&2Ie^9o^F3C4<5+qukWCt#YTxR1Mn1e~xO@tyAn`#1MsVXjUc^Q^^tr1_27D(Vzwb4PtnwI_4TW zzb%j!QE#V<0Zd6(&uyhmqj9yQbuy% z78A_8zZfMP?`8rNs4(pf36=<#kt4vsg5OMI z+&BDcjC-q6atur7efhgcLX##;K!V;S6j!+rNmQ5$ERZ%ayi*{=cm|SC%B0;j-NZjP0NSq~!lL*JK z22mNCNS>gtD`x+zYw zGpeLW5*@l^qNFH;PN6skIGRmpkRxg%pW}Zv+@nf@1SYN`n4k#?Vn~BiJT%fVo==Y7 zO+r#|5}Ih9L_#K`$Vww>qmU1dE#OE=5)&1QCuH2h8WoD;M;iafvzgtQxOPXJL=)ar z5QmB2&+vEbR3+Cij$C6a%OprtCqzj?1WG6+#x(L)#cb1fSLbpRsiZJYp(8Hh8u`tU zE#*LhBuu0tmZE;>cc0wO)e=ZsjRH5sHi9_EBk+PRaI4f zetvj(cxh?rTNy7euiM+(w6wI`+}yY4g@px^laozNO^b_*K|w(U1qE;N{r&x0*22Pq zt*vcrY;0m;qO-HJzrX*_pFbZTAK&zJbacEaZ)|K_T3Q+#8*OiI&&kOd7#OIouC}nS zFg7-RGx6=)x7ym;H%M>V^78V!y1K^3#)gN7-}L+W`RVKHzv187+Iq7)F)?v=b`}*G z85B+;xvM9lLqkK}UU@6S#l_Xz(<2}t@P_1#E@fqgw$L)zb1t&YPabhK8}xQA9+B2&L0)by4b3}JQc?&6A}b>U0)c+~_(4rgje&{z79O)Rvq?!wZ$zc1r@sZ{ z>dI<-e7w22xu&M3p`qa$dwROMP$<;h&F#%=0RaI%K0fE?=Pc4co}Zs-xb0ZP{d|Yl zso6ir-o6I!-dZ-_aO>1_@JRpo^N;YIv_;j9p+^uz(KzP#UORe1$*H)T3S0WE4a7QY zsD1(f(a@kSL|7lO4avpXg?V+DXzYR@3USyT)>?Svw1i>ia%7I6Oqfqnj2(f6p4|sF zW^sL9>%5|OX$BgsrXOWKnMRbGi}Q-|zWj4nY;mH`s8;{ywQ@7tRZn%XH`3;HSRvM3 zZQj}#!}5^(aIqhJw==amJvNvYd$BR!n*06a(8SqhyYXl;j*@pN{CXlGue}I6X;ERl+hHUOQ zrE*|li)&hor5pWw_A`YtpY+pHYWVl&J_RVj^7FDRr@__Jrk`c3RE`=85yoWN=Y*z& zpX%GM#@^2k4f5q1oz%qt_yuQM`$Mqtd8hw62($eaHGD5oeh|XX7dXq^|6=R-`0gF>v@OX{%R)gyNuqXo=o{!FJN5CXXyuBj*~SN5#( zsR@r%Oz&Akp`4|5^qyw`y#C$}`j?2U02Z5%oJ^3%p4{{=Pjr=j6d z#+fXkTGd7O`NlpI9;D7KD=>iugp5AQ1#iX3Jf6l4dja^59a zGcdKNq>`!CW%;lKUS7!cYLgW=;J*)|!akPEOQ03e^W=6M6fjYj%8~j#{;T}5lf{g6 z8m+l0-2%@BKthf^I3=@tx`VhWBHSNDm`L?CCeq#}LGJqa(+a;g5V*dKwt!rEBe@@H zhs7=l2@fCUwn+kZb(N@$^pMTmw*mo8T!rebn{a$9!_3&Cz$v^zWrm^p$#wl^GWP$eh)&?dVz4kp zU#^DU<#Ot=_UY6lp{!JR0HY-1=A+wI4`F1*i}# zI_WvTb`_%dk2tn*W_NqaWrLJH+cMMMC_CZGS@fq|C^SlTPFrqXqF99b<&&aerAa>J zuk;D&lw;E#9PVpOFD!{<*ob?%9p4}fBTpn9rF3-qkv=4p)0>Piq}^BYc?aSeZOurb zLWym~U#9ocHE_;0WXn_kvz}WR_ltk#jM?LXxbym2htWlJla?E^)ZTj(7d70<8804B zkxKk{80PlP8NEWUbit3`50gSn-)1TiJbvw`W9>3h+|}@>2VanMt`l-)=L@3M!C!T) zd7m$-9{R&iDFgt&mfrhrtenTa>tB#fBD0SEdi+t#K zA>d<0PDR4#D(274I|D4z;c7xLBQ$nt25xj>;6mBMq@~f{k@X*aOrJs=srb3Cw#EZd zqfvL~A>TG`bAR(@o_0f2+N{@9Ik~wVu5wXb(ra{Nkp)-c@E!G%9VPKYR9P_&tK}t$ zU{WvT&mGTV7tcK3;%b!(h+mMTpZI)hIxk+!za0c7ZSs@w`6jXHKWRw`7dzP0nZ(Wn zLg*6)PJMVPR1R;|6_{R02OEba@!5-(c=1_^F@|Zs3PA@r$|`CEYIbDI3FM|{*wKin7~u3qwL_#cBWA=Ac#5U#-nJju;l(E-CE@8^ zfG*imId~q(y9;cG{sRA8&QBtG4P;!@Qd867iI7@>c3juVX`beX-B9@sF_E8dEt6zs z_^b^%SO)y*t$a<)&i+LZBN-}v>b%XgJh&guzvGPXT%~Wul@R5M{^6HKeduRr>5s3W z{#ycnDgGwc9VgRRvtNTFrh?bNJaD1#%R5zK$s@q>99$aX``I9eU*x zly=M)#ZID7D_cBkx2d_6w8)%5o<-DVhhK$y6$1%# z6qJvIi21+zuU)HeetsfRelEaM2&ED)cQ&sSWcZw_O;Fl1K5j}7qTy9H;DP~{o^~;gf$2*_nS(dgXyOtm2vg;0!_qNr{H~E zD9DzDG)2nWMg*}sOZJC5nll&B@j9KM+?la%hg}Ika3S`}^IJh{ zpHQ-pLYt7KBJNkP7~;nrRp+d)naE%v-0F7ALz+ng-`CJUEeU)1*f+uJB{YJi*v}jw%KCC;c`esaOyb}fV2XgK*UZEJqJeV=IG{v8VoSw6r6ZU-w(&HQfeSu|J1`I6d~ zp(~9M95va7iTz4J$|d>bBuyg2dAOlzy=~9ybe2n3^b>M#H!b=v@th^o;g=~5bn>Di zYEOFeg4m~@F0z4NIbrEe|Lz3W{OX=-KF6Z++0Ywds|SZdBt0NL%DxA>`_Lc-17}%j zG-RZdmY*)Rp(alU7(=WnhF-nnO8*8oE*K#mj?=|^O|`>PI0NTYqj1e$s?~RCV{~;z z2;{2q>04ai6`!R)c$($^Et{{=Jxi)->W51&jJ~U z=FCLq*fP0G_VC=nNuf4KHp;k=YPNM!%7&14(5PvdN)No6kk!uH1yyh?GI$$~$JR5P zm1t_QxG2cu16pHsM^84zppsYE-%n+sN;)J-uIVZY`s^P40s7zSx4ATcSv;rP%ZbPf zUG>1o%8cCmd3j*5b1Zz>zun#zDTWy_KCveb8a$qAvj1t>n~wHGE(kKmr=FX8k?=jN zsI)%Qeqqsn0P^&~`K3(c=N6L9J!UqW zg)%3{(Ttr<*bLg0##jGHI3|j?2bQPAYxcqxw-s3R7g2dl?Qbs^=2AH%={Ss*>R7T` z%`a#NJR~x;=rpW_Tu^&0VSJJXjLQ#ubtQC&>WS4Q3ZZd*%uxL8XwThsJ^S&KcR39; zPkW7Z26Bv45o?~jM4Bls1O>hxqEwZ3fshgIaV*5Rh@^aZyA->Adk_eS*j&5p&8=(k zz{~A&m4yo(HZae%{vZ_&l-4-$@oV3HIG+r_&q;ZKrJsvnm>jq<>-0V!Uo}#g#Tb7Z zBell3E(4jufdKqqV6CTA9M({_(erK>Quup8~NFY>FS_`=R2mJ zR1OqNvLW?y=;W`z$@=RZA()aB6;FIV{-#o522@M24h;xWgMjO zXD!C!c7A6{AHlAaCcOBYym5?-^@CI+P&l0%^zJASK>=bG!kH$;B_F3*s#Ol#hB0;D z;*Sw>U50DUHiDH3}o7g1|bD*{yj6I@0-xzM2HIbAY_VqviRy^g-@}wGFDs=Uh|T{ zlkpH-bts)-k*sdr$&S9|I85KHe^uuXSqL4^4G>o$2<;V-d@I<;5=;kRD5Ve~zy~>8 zbp?YD#R_t{=#sJX^&~+oF~HI-1zt{tzRjT@LCTy?r>xoCQ0NmnsdxNnz5Hd5_HiBO zTPxdO7C)6=F>d^gV{(Co9N`0829HdqM$pj82VkM^x$&V7RG5HnU2$5dP^Ct>K2fr@ zB$f^$gj5(>AtmjOjsJb*XQO$bUKQm0pBA+oHIb&Ka|Mgg+L12 z4An%#3HdY!U>1DN*i0H#N(MTSUVIL;il)EV5~*g2v;<5S_=v*$$*pZ){|QQ*m7<_> zAHroq5c|El@ljyf)Ev11a!WSCG|f6?WSjm7(0-I!8L4Eg?)r`ogdg?(>MQKSQTY6l zj89$~(%gum6cFrkbt|*JRR5Yiv%l3$C%rx$%f8&}9`d*2ii#NzLW1=)F^rzpHw+<> z%GJX1-m?dRHS|YMo`inm0i-;z=+Pr6w9#lh6GvF1p<-<}c6r=%8p6=g!?B$VYB=SZ z(#PULgP7{Rv9$j}LfcVpgv_|4&~<}a5>X(hgdGGjJZ zkyXvn6qbnaD|W#aY{bN_DtE;1g4i}CWO~|oqWVSalK$+E|C!~ct)Q%N3JRr5`;QDY z^L>cpWtFZzOC;B44$E4FZn)Wf>?u3|6X|nUbH3vFD>5fS1z|vHP6M^z1Ygwh-&0_;EI#v2hr_(@D!j z)l0)&5t&A@%b5eiOs(!TD3(X<($qI563Rn72!W%Cp^`>nARx+T0ItlP-0Lg0@0TgM z0hYB}A&SQwDh=V)T{7W1 za9Oy#YZ)#>YeT~?!!dkTzt2P5cuNi(zJJUswo$%C0Rp%l`-KTfdWyYM#?Xe4FNoXl_e0BAgVvHWc5@a@@JHc_)5HC3RsX_-V^}--_gusnMOQxgE~;P zLTUEi<&_&<1R*F>D(8iJ3=3g^!Q%+YS&gTj<$m0@z;_}L^CB|8X2H=bc>znfyNK4y z9}AV0b3A_o&ewwVDm3$Q>RuLX=C0enLJG@PnPQBRN5GK;;TLAX!q45my*ydj>U2ZV0txu;)l_kSl`NJcI@G&Ng~F=dH>j6Ne@( zHPL^lS5EXJEbn$#aMSzdCYHit#@A(gSCw+^`LdRjG_8v2YlCP74u~~@L|stlvy=d1 zH#|T;UTC?wH^ISOy-}&WgxZuXx#PyWqNC3Qx(vpF1ou~m8#uU(m*bYqBcMglOG+jp zFqkQA_DybjKkcXZ?@|6IC5|e-;szw)8}RVLBjjr(}7VxT^CYFdxl{VHa9!e zdpojVrDt`u)?i>0fiX``Qz}T6G8`%tp!Rr7ozq45rIv4miZ5WItIKk$VIn`8h81J(hvz3XP-8Kt93KVwK;YqtN zrxIYHS0}{G`w7JTcb8IcvQG%t5SpWe7LSiY`vG$Op6+S82~7(z-aR0Ui*;G~4^7Ui zeL@7CWENKn!>+rV?jh+_$IBj?Z7Ci@QADZd#kg$mfAIt6t<8hOW9r!e=`GV2&4AK|Ksz>5-ZC6e#QXVRlEFHWVhuubm!>z)bvqjNbp* z;X5k?(3+UA%Z3@RN#$je5y37t*umz!9lGO3=;;@35(coAA%YR?Z;$EWLh-B`SYFg@ z@RP5YU&#Gat5u>4Q&JXPwC#rz(J4i0yNLV0Ni0H%%#;Zp>tX)I(tp6GBMOo3o6+y7(@K1Xr8YUjTd$5#gvs+YI&^QxyFe1=+yE@NtV~AFLz_u~20np(x@x1LURjk` zh>$FS^5|`IzeOb&-AoN0;|8o`?sKGRGKSJQVXk28agnP{`9p z4XL?aOeRaRt$r!N!l(_#=kBZpm{PW*H`MuBw4VX<|?u*}_(mvq;pm>NGjQ@4Z$#GUPC z`7W$JUG96Xk2?yb{KR{*+Az^!>S}RL2LCfXA2X#a%H}XOo;+1iiPEfUnq^x4D3%w(RfPMl z#C^s3oI<+4ZVVx_Ah~Q*=hLL$M9Yucx1Zrr4ZmX2{XSE5e)@eKIxSZ?NI*memMX;N z_$9sWN(m^IW}DwO#4nY@EojGJYVc~Zdi-}o^!~h0FbWIUb*RecM;I;0d~?vy@l^`R zNv7gHQYzz@ZSrB21H(t~PUQDe;(b1bhn+Y@JW95!+wO2X^eo64&Ge`6e044_zO2~V zi;G83Z+jQZXC3<}B{RZ=&PY@tq0~;yVD^DtCktyZXYu?3dYDORX=c3jm`?ouQNQj| zcs+k=cU&_)ryMJloIdUq1woa~q8)>BUILz$@7mo)-^5jw=l$Ftls{g0FG6cjfba(g zvF*v4LmC`MbVmREVazwIt`)9w69}dlC8ktI3XwmSKMiyU0~VaED#dpgRJXY#8HB@1{$lyB{jB{W3TwU^dzDbO*;%aoJpR*By2@a@$G<5&*c>ym zm7k(%NjXOmDW>(J-R(3l-Eg+eQ{SSZhm3A&7q$|q z9(-VURjV__1$pPq;VY_tn;~Tb3@uWKv7{?4VIHDe_k3Gk%2`k?FsjiKhju}vPfrVj zV)?#P-`R<6-8l1bdu|oIp9}Fs@%&eC_|2S9Ez`viN36*x418<^FHuTP8VszdSBn`w zHefS~wt>scj7;ezJG0Z-YlVkG*q`Chj}J-^g1#m{^0Sk7r@{7fp{SC(o#+mtV*j-L zZhAUQ5IsIB&~gxmW8@?f)~2h3mm17%k%gp2{3zrwId3+$W87(A^kt~OC1gG5>(3f^ zen#0i(=S;J1@FG%3y?LnABpq;uQhyHRY4I$s*wS(4rv5R1uk^BRJbBvjkcztf5RzJ zL%%G@mx}d^!QcJ%nt!m&j->w@{<(SmibmM1L-4)-59{xLJVL)}UI!o035U8+;A5uU z`I1#6P5D$T45Qits6Z*bd91|l0w<1f4e<|-YZ+;7MmswUEM7M1`=O#6l(Ja}gmO2e z?){H0)X<3u3|!~$G*4bc-s`*Vk-fh6i{6FY0o!84Y;a#uivkONY>AW<>uv^Q005$$ z;QE=4n-C*6fIaS7KRnd@YSfZ{AvclNw|O^k8~6EH(u^@|z~Adk8arzSvE*@-w}{|! z>oy2?Z~qTh9eMuzDc#>zmLv6FzZtbu6Q@&jSPQ2;5ir;$dp7w?6tFqm={aKutqdsR z{~ z7D_4H(SF>iBz5CJNPms9PFiiYeia4bE`EKN*DNF9AUmvR8V>;FYV(^hBtWNkd_|gM7}L775E(pg&$2dGxz`ru80SM>bFZP!iaDYr7k`Y7q$J0;ZZGvf zOZGIFq(eJm#`{O5%<{+_7D(|N%k>rXlb0mI(RRsvxUyzNsj-z+TZAFf z+1wDn8CB%&52^=vP`fo2XyIZTlw{)hZ^}|KK6(u5HGU=3bMn zPN$?84zvnLkhPMcV^|2~+IoWjQ~R%5NQ4%K6uXXhcqeg-(qmIdo*uZf{<8=98hhAZ z%VHK!lgB>%uUMdh*zbtia7OA+r3OiKLdz)W5Lr6Jtx zw0&&C!shr%i9`#sTH^zw#D8$Xuy?9xgq<6$R@Ln+D2PP(`ADW)YhJzRs*GgOtB3Bo zXGveBfKH!%CSwz8p}c&6xbI_$@8cCX6+Dy|i0jfAjK9k`P|beQgdP=0_ zkHOf?dL}l+Ra6vAaMG_QCKs8e?Uj*c5i0G}lhBo5mhq@hh zRXn3H?S8cSa7t)uaB1oz&P;2(kN?=hHq#?Nu8|kkVGc84WwM?s<5{n_lVao@1TcJw zQQa3K@|Vo%*9P>V()!DV3bW}TZ$kuidXSxdN8a&=O6tXYy$7-vcF8-z5zfj-&GEy8 z8W=E&8Pjp)3E?IELMv_xW>3Y&u_~urre=Vd0=|ElN^mj}Y3=}Ag_tdbSZr9si$emM z?2UUWc*3_ve3x&v#5=}+tgvvLaVUV1Lt#)WR&g0|mhU#YR24i^(gu#2BD5TJY|&dt zq%$U_$9O5@`>%+-&c_9CL?m0y0YzA8mf`~b1b^OUt_u$PH2y*l#_@5qV7}nl+A6kY z-{-6RRYC4`NjaF^#D)kn)W8D#P#rHNM%fM1TXxc{%=L(49RvD&kT%4-UGDa*7(Cea zQ=wcp+d>n8U4~yM;Qc&P*)8FMoj#n7$O+(;b#-MMb0+bURTTj})s;p8hzLYA04{CK zeU4LLg;vCZ$doFPtcWfJLWdC{bR;9Un}EOD4&DT_X?7HZBcEI%(eAEy7JSdB?$g$X5mC* z^ra&JsjuNuk)mMp7LY<@i??03!Pw+Sv_Y|iV z!z0~k@3^JX>ik?8!louIhx>Mdzr3)&j6;`vG*e=xSurI-wHekhH)QNWYWuZi#XW9H z>u}foUZ*s#ua|HT>E660ZvjmjT9;ffWqSWj+{BaPh6UtMrq{$lp*#q|o`;oWovtpNws2=lwPr#Xv71CM=Vkkbe& zo&t+}CPi7~j4aV#!BS#kWB?@)ZMy7XD2RPIdn~RM4>$(}WpNZ}M;-!CLx~NL@qO`2CWoHDFQ|RWj z?xQdt72U_Ec^4ih!rwWpx`6O0zQrF~s-t8&)0{-ZJ!)IrF`dkZ;}FMPE4pwRxT$4j z^}h)HaITH`6wWCiyJdO^x&8gLsC4{!gm)r~W?u&~^9Ov!+%E4z>(E zKx(%KZRLAe?$oDl6w^|f6_PRL0y2c@dFPnljQyoA=-Ilgu#&vzPrY=GGhw^Ob#*80 zKn5MEZobCau4!3P$)gg4067!FfU3p;;5ySOub;`m*vj$B-q_o|+9KYn4bL$T!FAtN z`_Bd*?;qmu%3o@z8$JvZ02oTNt;jP|G_}-};}Nvy+=f!w(`wml!`hgN>yI+ki=56U zTS<42aTp@MMmXDba~39iP<9@ag_ z?f_2x#EXVr-A@3-@F2uJeU-e<`i$`>VensIGZL`C?O)eHBZ- z=zQ!@auE9c=LQYUJ>9RQ4Bq)vlR$mjnU zkWvYTKyhl}$GlLE?mu_Y=>`2o?(`aOt9h#f$37Y7w#N5Uy28Zig?dHBh3K}JVX};N zG3ufn8x113RDjxFzkdB43VcY1Zm#$3ie1AoIjcS3;2yv=^=y55$uZ%HmqgZZnWYT< ztLt@qct;?PHZmSM^5?X%|K8DpO5ryuab}QZdGnxbc$BVfLZt3z3=C(H}SI$PBw-5^8-lY1_c=}*SHGcutU@A8wr%o*-|T3wC{y0i@-F-|g?XL4|AiRHk9>iNdi(e+`Gge60=7B7lWKL-rI0r>0^o#6m^J zo4|z=i&DTwFo=*J(;FiU6pIl#zNU};?_{hOUCtfpkvG(BEpB$Go^+ter`_|S>V-G! zUt;nS-`~|IJKQ_!Yob3m?xBfIPALB71$~-07ZEb^rdU=ezg)nn#1Ui*DSbjJCcRe-Sl>VKzZ<~Q#pGJ<_epl!)8&9r1tS;2; z)WzT2sxkV!Mn=|J}W0QW+AaVw*d? z%zkg z9`SEo_@F3lz9AubDlx6~*Qx(Vv%dGp6w$bMA)uzaIv~aD8c=29TZ%nzGrW!3=Nqyl zzO3G?R=pdyW4Fbkq|u6pu`Yqf>Q5m8rCTa8^dNLrcB{Jza9@_~9rbE!9==$vZ4P#) z)njCdK*3ydXuvGAA4c&&tEfe~;Q@jg%m4>-Om?@xe`MAB6alq=Zu&R@9^afk_^Y+# z_ai{~(87L!Zi<;8*BI;Z`#}3W%V+=yHp~Y4g#5$3PHaBM0%SlDJy7!iLw)z((+(kL zkPuG3xr?;Fh*Z?Uvv@%zqW-5=KN=(br?bVlVGAYGdR|uOdRH>IACs$Hc)d%&1^M5YQd_Abw9YbV|qvN6-OQIqk zgMU73p7N0QCNfm0Z@$l_D}?M{b(MBG!#3Vejio3v4Y-r(!<>JX>z#5paDMxbj4(sK zw{Y6uY`(1Yce`ePKdfUn6~%>0_4pDR^zOrLteR(ljbGBF9&ZqlO1wamN6i+bK zDnk4kR$o+Y+mC6-yM)4YBs>?%SsMjfZzCfFcp+baaXpLU&q>I?w6Ay(Dl0m>B_sBO z8JaRK#FJWTp@F+gX8*aI-c0uIzO1676*etc@n-q{xrvoH%iwk_P?sU6y9*f8C_(BT zjzlD^8DvL@6)C zSSU!lwTA?$yBShaN+7n*niSpf9 zOScV!;mSU5se6)bES1+Hu(Ui?7CfhcgqMU~Xgi=&!lJ>hlNiYnVDz&Z=Ka~Iq?0fz zk2o3^(HfMdjJf)w*@zN*C)J2)_p+4j)*mlauof|9*N*BLc?%75$bV;#qQF1W;Kk0@ z`=H(dL~emaVK$y)1&p%!v_UlnJ~1?1;$nrnKxPHtWV}Lfft$5PYU9wAEv^7|ws)9a zQ0vpo&Fr`BS`MW>Vx+3R1Ru)9m|qXxj$#}hO=BT}p#(*mb}POT9q`_ssnnb45RP#A zRW#s*Tm@EHaUtWSZHoWYw>&>g(EE0?eEoyVXZP&44d44pT9Do{#{=#`A3Q!5Ek}wN zp&?;aC#U%uh8SK%C#}>g*^ho05WFBpQ%Rg;HZpvrIrxAwK(|ZjSc4ufI1F9$-crVl zR-v;yzEqox^~4Nt)K-6O4P0%`k`-OO?7fUK%WT@^iuo>ty#@V5TvbL3uOOZmjOI~^qph>7{K_KoUiS*bmraGM3aR*Os}J5Fht5Kd)`A1eBk zgxJ)Bib-Ys2VUH8MhZk0NTOX3E5|G_tWw8TGY4PM5P;`0F=ZA)_$LLJPaL!0k|d$k zU?cSROje)tU;y^kqGM5HTOhq>!fS;{Eo@ywy#uhNi<<7tGl>Dyq*PAUDWr2(w96Y7 z*RQi8`nhbQxP2T)g|`Y%LeP}~OWq5D`fISBOg%uH^M!jhSZL%|97y|`AX~1<(!Ui#acx&6En;f*ATYQc~z1DW-0+?K<4h`%K2@;vrmzHhGqa zMoKS}KLTJ%geL!sO%TA%<3u6N!Jbet!bnX9BCRu3io(b16kpM6*&J#e*YqWW^b(@l z8Z?z&uQ-Z}-afl?MPE9?#U{jV0@$nCPLcmCQkm_38~m-P(7y$VvYyNS7Rvt{O@6@^ z5dS$*YX9T;#0BdJThuOT+>e4dOGYB%Pl>{+iTqrBOThyChkH@HE3uJOlvbyTp^Le!TUW5N@+_!)*nT{<_5C zi7|Zg{p;H~d6Ry_Nfowu@~%djf{Xw4f+KhWLEf@;Bqp!PYFg#ov^-)?a)%37I8|g1 zz|t?Navq3}8qq?$dxhcNb~DlVtrI0~VYbyykHMHeF3brjjD`-h-c3Z;ATk01agLka z^{0bOn^8FlR0I7v(&X+t6k#=6CZBk)e{0TZ!9q;66zJtjSYtxZJB1ixNmIo)fU!F} zZk=|${Bg;DY`j>qX))ALI>b1?sVxG4UUGlH36BQwC1>`36c-828J?z0j*|3JXdpyW zVMhOAF-Z2;)4+68tvPjsL4w5}~*V3%M=SQ{d&?WsGo&ulc)XVZHz%*wseRxPs6?pHX( zE#ATd0h!U=c@;IZjo!MaO^=Sxm@;y;Fpe$v6uanOve?Rp%|LzDX-A97^ZrtZN~)YH zE<4|zD}-E_2CM+X!)1b}G@foe?3`?7^&{D}h*8fV(z1tzMQkV2byWY1-4(s+sz6B* z^p5kr|D!x*`)J_!wI=}~Ocx`@eMbgC91+a?Mkutypu+pO~zAwJ{TdX~&{8)_l>J_uhh%=#sQ1)P4rfWv{ssJcr*W zQrrFO&Tl2iXU#em*rc5~oTyUxn1rx(l3E6nuc7X=Wi3IYbF-{a zM1rY{j{eHkU!2maB}O&q=moCAMnOfDr#18g9Xqa`Nf?rX>v`3NO!wj#N9w|)zmzhg z4!-{$%br5d7JVlS>1Ci})QG|xW{y6kk4~f(lEN*!Te(wX*#h9OL2E*Udnq~1fSu>jdCGzquIrur? zv3i!(JFhoJ_m_pEiBwRF3VcpXI_r#(n_m|CO1p`O-`z(}6h}i7R%1tg<%tyTt`lk! zy^KOYg2OQ7{Vz*)K-#`iv+3CCZQwx8grgvx9&@o_?l8Yt%!jutoF6@BVAOE@l*_!B zT&dr5bu=IR^tG|G(`xe*NJGQ*(VEni%sx5+=eP+gGB^@)&=Jhyc!*PE0toY)sR*VD z`Ib#m)#3=M5di4=hf>gd`7y3|$JQ`NJc@6kRVWZo8k#Q4DV$V$f*dy(5wVagUUPmG zUG|x@qy5DvENEH#spdHIUuNd-N(p*esw6TJT7t-|U**k<6@$L6C!AvupF+g~R zMt!@tz$=shNHp$zg}EySWLH+3LIKtPClAeF{3mYvJ*ueummT`pN>980!hw)Xl(?qN zEJB5+i5I~?&tA{RR&Pf;!Wjq(3D-%0@XJ)1F6OS*Oa1rTSbe9`dKIhx^}-;c6uvg; z65@%MLm~95Vw@F<=Z+RoFy(chjYA3Ngpg1S6{Ls%dMIxaVL0Jqr3D=;2M|B`j2Lft zJMXqlTgyI(O|`qfelb?OR6$MTpm4=?IC=wPLV1 z?gedaUCbAAbK1ZKnuVMQQRM>Y;sEGjXmS2wtnp^^KE;1xB9}%*v2gKg-2G8Ls2<_B zIg859wptK88+t*f_~Wa|MBvSw{ON>XlCHQ8`W9!4f5-}YpBV$`Jz{^#37JFlaj05O zG%*MsNy78P;9PX9bjyAXWmQ#m?IR_~T+Dhpk_`9U^nLzdx@`q7dFg>znIZs05RiXl zP&XnS@)Y2ZM2_ld@LibBkh<%w6OSXH+QDF%m7NgLLSfKghzcWmtGN$&#*t}z$`pYe zLZ`XoxOFG}H^4`xuy+H$mu$wbi2?pNx!@R(RDYU*c9326&rX^owQt7B9 z^9h+^RGO-mEKW|lLT@r|Y?;JWf6f7Y^Z48DcRHDH4yvsZm27W^#S$ZA#rRo9ygsqj2D-Y-Hii45JD@E7KFY$zU%%7x=o4?!{&G)@0 zVvzDOXrYSx&mkv2PSmTnATkJE&5&bv0J)z+Y(}K(P7ga460OMDtW}J2$I6nRdDwBu zG$Q;&qpxEHUIRk5bC$H2nlv1vHl9pu|4i7;b9NO=LGyKze(aWX*I-q!739lD^reYQYpxT_k?Ym#_Uk>XPfS6+Lq_%7dM?#?ynnY z2+=!ouY_9Z_Zle52OJhNzNsEe^Wx&pmlIL7s?Je~+#VIZ^SND~xUP*KpA6dqM5hSa zEdeO55sQVOdJEb~LDKl>?+6llUM}(Rjzm`ZkmG=O=awdM{keS(mk}|Id~=gkvHxT) z;X<+(ndK&2_XN=q^0M zM2p9?Eg*?Rv4ivoAh!TS%@4^#;>f5DvMNb~v_Ukm#sv(bv6aN3yvUKmo2sRYglud5 z+9&3+U4}aiB?l=k1)hnnk}AwHs0+tQe4% zx-)0f^wE#XfJ4$ia2&4@G-Ql#%Hus1ry;D0dlg^gu7s{?L6NLdM1pXKZ^@ygy=O+F z^F)u}VTvR$+uCv_RUfk3l{_R46vHur;83lK(|Ci$xY<_8*PK>XkS}sd6z$fY*Wg=h zDEYjaeq=!F@F3f7c~T_e3EXXMHGwF8CFPNVWB89W8aqrHr~=>+G>SJS_f^Yjk-PND zB9d1j6245i1R}k4ckEgqRx6T(mjoj9flA|XHN~yQKR%wO4OkTg2g?l<(WqPv9H7B* zU|Y2^gFMSp9z>)_whJT>iSRA@G`)_?QxVLGK6AeXiA3(;C#9$9qhI2XJcK%itB!xg zgQL8>q=Li&8ZyQ?4Z=}O+p1FY3g}7>@_CrdFOs}HyM=Gz(4Bm%ukQ9mEfjUiBVG@H z)VGQNQFw5>l5-#h2iL~VGHz8A<1eG$K#F4;mo&tFWF^jk_nsshxkw7OK`Yn$^M4Z#D&8-AP^5{}{NUJKR`A=BY!<+`k z@os))WhJ*Nj)MjH%APv1<4}^s4EH?VZ;8u7Akuk$+QEDax$c~7ZEi_5ENNFf%tsHJ zG}ux-od$5^zY7|`u}z4Bf6V$y+T|`?BoYZ1Ndb?Nrv#E?)xCJ~jlGEod(X$?Ob8&! zfi8L6{GYwM`EfCd!T_Esp<*E^QIhdhrKH+SrH!g;rxI@)A1R4QEKJzUZW;?Vv$C}0T!*hsKoV^n>i4i0J{PGccCwm6QJ;VSpP?Nwi#B)&)%ph(s@l7k`1IJ(x2 zYv(@RLf=BmI)7O}`~cw|xTkm=@2U;=8z<3-=ZZzuaMiI^ISQN{D2{*Om$H50kRKL_ z?8{!no$@QfCGaMl?njQx3uP}&zI*oU1wpz8$jt{Yk4_x!sSQ4HNWf9=;$W|`;`rVq z;ZAuJ$%?p=aFn#~$ZKM+zk2le{S^TTC%Wrb&ks##r`oC-;7|=>NOcsc(ZS&c`F!K} z)`PnqURVkeIpMQLNz$C|qvP^2<=YoNzI7nJVcmRnYD}Lud~N6$Cx$dD&jF2)!>Z#) zi-dhkt0X>Grn{0HIX;P3+MUG}v6t>mcdV(`N5>AhM)hTldYu~8>RRO}kVD>H6E%&m zP7=Q_nBRty%p*c*Mu|Eji-8xGW^O_&hW&EP^-gKZ7?=O|?hYtA)d$MvnzGI66clFB+H~ zRvkZ1!cnqvr5xYI8F#FVFa$P>iH!FpJ2x#medhH3ji7*qR0c6<(W_Cb)xzS?x0tRn zj-Ov{#pY{~$QgGfDNBm3Owy>6ojW%nt3wRYsCx=(I1VX}1~`VnVb$>)lO%>o!kamS zq?#3zdcBxihYz{Z8<2E*xf|h>n9j%uF(i$K<4BQXj;*YI`H;K22*TYbB*|)E5??1_ zVNih-5|2R*a;&oFj^De}6h#tVDAd4zZxtDl0EKY`bRfoFp`b>f8u^sXjy~NI$Xn?O zg`goMwY_0RG~plet$*Ex5d^~sH4;)C)~xzKGQ^7lg9fszBYSrdhpBa9AO)vEj;M`} zzKHPJt<HgZE9%j$qD(=P z7D%ZCjimLe{)QK1ClZ-VGFd8>TCG-vA&gI;sZc{OlGLzf)n}4ulq5YSM8OXV2NV@r zlv2s0U}O@pMASI?E`q3tCxJOvLowz=4P&O_+ZIV^!q7r7BvGr4nNP%%(8MR>LJPq#jai6_ zkVN7vn2yRYjoC^hB$0GWn8oa;m&7M(8hMJas>bY0000000000GXaNm7_;dl RggF2J002ovPDHLkV1m>0=9d5f literal 0 HcmV?d00001 diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 72ebd3993b5..bcb0e2acff4 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -4,6 +4,7 @@ "disabled_cannot_delete_trash": "A Trash Bin or Waste Chute is required", "disabled_off_deck": "Off-deck labware cannot be modified unless on starting deck state.", "disabled_step_creation": "New steps cannot be added in Batch Edit mode.", + "disabled_no_space_additional_items": "No space for this combination of staging area slots and modules.", "not_in_beta": "ⓘ Coming Soon", "step_description": { From b574e89cbd9ee50718084f0081122a8ac7f73873 Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 20 Nov 2023 14:03:43 -0500 Subject: [PATCH 04/11] fix(app): fix protocol card display issue for a failed analysis case (#14026) * fix(app): fix protocol card display issue for a failed analysis case --- .../pages/ProtocolDashboard/ProtocolCard.tsx | 9 +++++---- .../__tests__/ProtocolCard.test.tsx | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx index 8b9d586b876..e52af66025a 100644 --- a/app/src/pages/ProtocolDashboard/ProtocolCard.tsx +++ b/app/src/pages/ProtocolDashboard/ProtocolCard.tsx @@ -70,10 +70,11 @@ export function ProtocolCard(props: { ) const isFailedAnalysis = - (mostRecentAnalysis != null && - 'result' in mostRecentAnalysis && - (mostRecentAnalysis.result === 'error' || - mostRecentAnalysis.result === 'not-ok')) ?? + (mostRecentAnalysis == null || + (mostRecentAnalysis != null && + 'result' in mostRecentAnalysis && + (mostRecentAnalysis.result === 'error' || + mostRecentAnalysis.result === 'not-ok'))) ?? false const handleProtocolClick = ( diff --git a/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx b/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx index 46abf73a30d..1fbf3feb798 100644 --- a/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx +++ b/app/src/pages/ProtocolDashboard/__tests__/ProtocolCard.test.tsx @@ -125,4 +125,23 @@ describe('ProtocolCard', () => { ) getByText('Delete protocol') }) + + it('should display the analysis failed error modal when clicking on the protocol when doing a long pressing - undefined case', async () => { + mockUseProtocolAnalysisAsDocumentQuery.mockReturnValue({ + data: undefined as any, + } as UseQueryResult) + const [{ getByText, getByLabelText }] = render() + const name = getByText('yay mock protocol') + fireEvent.mouseDown(name) + jest.advanceTimersByTime(1005) + expect(props.longPress).toHaveBeenCalled() + getByLabelText('failedAnalysis_icon') + getByText('Failed analysis') + getByText('yay mock protocol').click() + getByText('Protocol analysis failed') + getByText( + 'Delete the protocol, make changes to address the error, and resend the protocol to this robot from the Opentrons App.' + ) + getByText('Delete protocol') + }) }) From 97d8c5a7d0d359a72766801b1e81b4b2eacd1ad8 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:18:15 -0500 Subject: [PATCH 05/11] feat(step-generation): create aspirateInPlace command creator (#14027) Create aspirateInPlace atomic command creator with necessary params. Write test for new command. Wireup command closes RAUT-874 --- .../src/__tests__/aspirateInPlace.test.ts | 41 +++++++++++++++++++ .../commandCreators/atomic/aspirateInPlace.ts | 30 ++++++++++++++ .../src/commandCreators/atomic/index.ts | 2 + .../inPlaceCommandUpdates.ts | 9 ++++ .../src/getNextRobotStateAndWarnings/index.ts | 9 ++++ 5 files changed, 91 insertions(+) create mode 100644 step-generation/src/__tests__/aspirateInPlace.test.ts create mode 100644 step-generation/src/commandCreators/atomic/aspirateInPlace.ts diff --git a/step-generation/src/__tests__/aspirateInPlace.test.ts b/step-generation/src/__tests__/aspirateInPlace.test.ts new file mode 100644 index 00000000000..9d2a1ecd97f --- /dev/null +++ b/step-generation/src/__tests__/aspirateInPlace.test.ts @@ -0,0 +1,41 @@ +import { + makeContext, + getRobotStateWithTipStandard, + getSuccessResult, +} from '../fixtures' +import { aspirateInPlace } from '../commandCreators/atomic' +import type { RobotState, InvariantContext } from '../types' +import type { AspirateInPlaceArgs } from '../commandCreators/atomic/aspirateInPlace' + +describe('aspirateInPlace', () => { + let invariantContext: InvariantContext + let robotStateWithTip: RobotState + + const mockId = 'mockId' + const mockFlowRate = 10 + const mockVolume = 10 + beforeEach(() => { + invariantContext = makeContext() + robotStateWithTip = getRobotStateWithTipStandard(invariantContext) + }) + it('aspirate in place', () => { + const params: AspirateInPlaceArgs = { + pipetteId: mockId, + flowRate: mockFlowRate, + volume: mockVolume, + } + const result = aspirateInPlace(params, invariantContext, robotStateWithTip) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + { + commandType: 'aspirateInPlace', + key: expect.any(String), + params: { + pipetteId: mockId, + volume: mockVolume, + flowRate: mockFlowRate, + }, + }, + ]) + }) +}) diff --git a/step-generation/src/commandCreators/atomic/aspirateInPlace.ts b/step-generation/src/commandCreators/atomic/aspirateInPlace.ts new file mode 100644 index 00000000000..33d9dd3c0cb --- /dev/null +++ b/step-generation/src/commandCreators/atomic/aspirateInPlace.ts @@ -0,0 +1,30 @@ +import { uuid } from '../../utils' +import type { CommandCreator } from '../../types' +export interface AspirateInPlaceArgs { + pipetteId: string + volume: number + flowRate: number +} + +export const aspirateInPlace: CommandCreator = ( + args, + invariantContext, + prevRobotState +) => { + const { pipetteId, volume, flowRate } = args + + const commands = [ + { + commandType: 'aspirateInPlace' as const, + key: uuid(), + params: { + pipetteId, + volume, + flowRate, + }, + }, + ] + return { + commands, + } +} diff --git a/step-generation/src/commandCreators/atomic/index.ts b/step-generation/src/commandCreators/atomic/index.ts index 5674211fad0..3a6aeae7c16 100644 --- a/step-generation/src/commandCreators/atomic/index.ts +++ b/step-generation/src/commandCreators/atomic/index.ts @@ -1,4 +1,5 @@ import { aspirate } from './aspirate' +import { aspirateInPlace } from './aspirateInPlace' import { blowout } from './blowout' import { blowOutInPlace } from './blowOutInPlace' import { deactivateTemperature } from './deactivateTemperature' @@ -19,6 +20,7 @@ import { touchTip } from './touchTip' import { waitForTemperature } from './waitForTemperature' export { aspirate, + aspirateInPlace, blowout, blowOutInPlace, deactivateTemperature, diff --git a/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts b/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts index 19028db007f..739a9a282be 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts @@ -1,8 +1,17 @@ +import type { AspirateInPlaceArgs } from '../commandCreators/atomic/aspirateInPlace' import type { BlowOutInPlaceArgs } from '../commandCreators/atomic/blowOutInPlace' import type { DispenseInPlaceArgs } from '../commandCreators/atomic/dispenseInPlace' import type { DropTipInPlaceArgs } from '../commandCreators/atomic/dropTipInPlace' import type { InvariantContext, RobotStateAndWarnings } from '../types' +export const forAspirateInPlace = ( + params: AspirateInPlaceArgs, + invariantContext: InvariantContext, + robotStateAndWarnings: RobotStateAndWarnings +): void => { + // TODO(jr, 11/6/23): update state +} + export const forDispenseInPlace = ( params: DispenseInPlaceArgs, invariantContext: InvariantContext, diff --git a/step-generation/src/getNextRobotStateAndWarnings/index.ts b/step-generation/src/getNextRobotStateAndWarnings/index.ts index 5ae8cc86943..e559698b664 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/index.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/index.ts @@ -34,6 +34,7 @@ import { } from './heaterShakerUpdates' import { forMoveLabware } from './forMoveLabware' import { + forAspirateInPlace, forBlowOutInPlace, forDispenseInPlace, forDropTipInPlace, @@ -97,6 +98,14 @@ function _getNextRobotStateAndWarningsSingleCommand( forMoveLabware(command.params, invariantContext, robotStateAndWarnings) break + case 'aspirateInPlace': + forAspirateInPlace( + command.params, + invariantContext, + robotStateAndWarnings + ) + break + case 'dropTipInPlace': forDropTipInPlace(command.params, invariantContext, robotStateAndWarnings) break From e34853526fc1b8add9226a5cb2ecc0fce9410c4a Mon Sep 17 00:00:00 2001 From: Jethary Rader <66035149+jerader@users.noreply.github.com> Date: Mon, 20 Nov 2023 15:02:20 -0500 Subject: [PATCH 06/11] refactor(protocol-designer): change tiprack default slot and copy updates (#14028) closes RAUT-875 --- .../modals/CreateFileWizard/PipetteTypeTile.tsx | 4 ++-- .../components/modals/CreateFileWizard/index.tsx | 8 +++++++- .../__tests__/EditModulesModal.test.tsx | 4 ++-- protocol-designer/src/localization/en/alert.json | 2 +- protocol-designer/src/localization/en/modal.json | 14 +++++++------- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx index f08d7338d7d..7b9f1135f2d 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx @@ -46,7 +46,7 @@ export function FirstPipetteTypeTile( mount={mount} allowNoPipette={false} display96Channel={allow96Channel} - tileHeader={i18n.t('modal.create_file_wizard.choose_first_pipette')} + tileHeader={i18n.t('modal.create_file_wizard.choose_left_pipette')} /> ) } @@ -66,7 +66,7 @@ export function SecondPipetteTypeTile( mount={RIGHT} allowNoPipette display96Channel={false} - tileHeader={i18n.t('modal.create_file_wizard.choose_second_pipette')} + tileHeader={i18n.t('modal.create_file_wizard.choose_right_pipette')} /> ) } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index da77d891ae2..a29791d787d 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -263,9 +263,15 @@ export function CreateFileWizard(): JSX.Element | null { const newTiprackModels: string[] = uniq( pipettes.map(pipette => pipette.tiprackDefURI) ) - newTiprackModels.forEach(tiprackDefURI => { + newTiprackModels.forEach((tiprackDefURI, index) => { + const ot2Slots = index === 0 ? '2' : '5' + const flexSlots = index === 0 ? 'C2' : 'B2' dispatch( labwareIngredActions.createContainer({ + slot: + values.fields.robotType === FLEX_ROBOT_TYPE + ? flexSlots + : ot2Slots, labwareDefURI: tiprackDefURI, adapterUnderLabwareDefURI: values.pipettesByMount.left.pipetteName === 'p1000_96' diff --git a/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx index 056a1c770cd..3dec6386db7 100644 --- a/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx +++ b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx @@ -162,7 +162,7 @@ describe('Edit Modules Modal', () => { getLabwareIsCompatibleMock.mockReturnValue(false) const wrapper = render(props) expect(wrapper.find(SlotDropdown).childAt(0).prop('error')).toMatch( - 'Slot 1 is occupied. Clear the slot to continue.' + 'Slot 1 is occupied. Navigate to the design tab and remove the labware or remove the additional item to continue.' ) }) @@ -171,7 +171,7 @@ describe('Edit Modules Modal', () => { getSlotIdsBlockedBySpanningMock.mockReturnValue(['1']) // 1 is default slot const wrapper = render(props) expect(wrapper.find(SlotDropdown).childAt(0).prop('error')).toMatch( - 'Slot 1 is occupied. Clear the slot to continue.' + 'Slot 1 is occupied. Navigate to the design tab and remove the labware or remove the additional item to continue.' ) }) diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index d4412ea07a8..1b87395838b 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -197,7 +197,7 @@ "module_placement": { "SLOT_OCCUPIED": { "title": "Cannot place module", - "body": "Slot {{selectedSlot}} is occupied. Clear the slot to continue." + "body": "Slot {{selectedSlot}} is occupied. Navigate to the design tab and remove the labware or remove the additional item to continue." }, "HEATER_SHAKER_ADJACENT_LABWARE_TOO_TALL": { "title": "Cannot place module", diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index db60f68231c..81a9239a4d0 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -70,26 +70,26 @@ } }, "create_file_wizard": { - "choose_additional_items": "Choose additional items", "add_optional_info": "Add more information, if you like (you can change this later).", + "choose_additional_items": "Choose additional items", "choose_at_least_one_tip_rack": "Choose at least one tiprack for this pipette", - "choose_first_pipette": "Choose first pipette", - "choose_second_pipette": "Choose second pipette", + "choose_left_pipette": "Choose left pipette", + "choose_right_pipette": "Choose right pipette", "choose_robot_type": "Choose robot type", "choose_tips_for_pipette": "Choose tips for {{pipetteName}}", "create_new_protocol": "Create New Protocol", "custom_tiprack": "Custom tips", - "name_your_protocol": "Name your protocol.", "description": "Description", + "name_your_protocol": "Name your protocol.", "organization_or_author": "Organization/Author", "pipette_type": "Pipette Type", - "protocol_name": "Protocol Name", "protocol_name_and_description": "Protocol name and description", + "protocol_name": "Protocol Name", "review_file_details": "Review file details", "robot_type": "Robot Type", + "staging_areas": "Staging area slots", "upload_tiprack": "Upload a custom tiprack to select its definition", - "upload": "Upload", - "staging_areas": "Staging area slots" + "upload": "Upload" }, "well_order": { "title": "Well Order", From 7239a530277f3484dd882881834231845a67a9ec Mon Sep 17 00:00:00 2001 From: Ed Cormany Date: Mon, 20 Nov 2023 16:47:35 -0500 Subject: [PATCH 07/11] docs(api): better H2 structure for API Reference page (#14016) --- api/docs/v2/new_protocol_api.rst | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index 5166d53f035..c54434eca8e 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -2,41 +2,46 @@ .. _protocol-api-reference: +*********************** API Version 2 Reference -======================= +*********************** .. _protocol_api-protocols-and-instruments: -Protocols and Instruments -------------------------- +Protocols +========= .. module:: opentrons.protocol_api .. autoclass:: opentrons.protocol_api.ProtocolContext :members: - :exclude-members: location_cache, cleanup, clear_commands, load_waste_chute + :exclude-members: location_cache, cleanup, clear_commands +Instruments +=========== .. autoclass:: opentrons.protocol_api.InstrumentContext :members: - :exclude-members: delay, configure_nozzle_layout, prepare_to_aspirate - -.. autoclass:: opentrons.protocol_api.Liquid + :exclude-members: delay .. _protocol-api-labware: -Labware and Wells ------------------ +Labware +======= .. autoclass:: opentrons.protocol_api.Labware :members: :exclude-members: next_tip, use_tips, previous_tip, return_tips +Wells and Liquids +================= .. autoclass:: opentrons.protocol_api.Well :members: :exclude-members: geometry +.. autoclass:: opentrons.protocol_api.Liquid + .. _protocol-api-modules: Modules -------- +======= .. autoclass:: opentrons.protocol_api.HeaterShakerContext :members: @@ -66,8 +71,8 @@ Modules .. _protocol-api-types: -Useful Types and Definitions ----------------------------- +Useful Types +============ .. The opentrons.types module contains a mixture of public Protocol API things and private internal things. @@ -80,7 +85,7 @@ Useful Types and Definitions :no-value: Executing and Simulating Protocols ----------------------------------- +================================== .. automodule:: opentrons.execute :members: From 5367bdd839c56d92ac861eca94ed23950b58e276 Mon Sep 17 00:00:00 2001 From: koji Date: Tue, 21 Nov 2023 10:08:12 -0500 Subject: [PATCH 08/11] refactor(components): make entire fixture area clickable in deckconfigurator (#14030) * refactor(components): make entire fixture area clickable in deck configurator --- .../DeckConfigurator/EmptyConfigFixture.tsx | 18 ++++++------- .../StagingAreaConfigFixture.tsx | 25 ++++++++++--------- .../TrashBinConfigFixture.tsx | 25 ++++++++++--------- .../WasteChuteConfigFixture.tsx | 25 ++++++++++--------- 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx index c0294942b69..f20f402eed6 100644 --- a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { css } from 'styled-components' import { Icon } from '../../icons' -import { Btn, Flex } from '../../primitives' +import { Btn } from '../../primitives' import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles' import { BORDERS, COLORS } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' @@ -48,20 +48,18 @@ export function EmptyConfigFixture( flexProps={{ flex: '1' }} foreignObjectProps={{ flex: '1' }} > - - handleClickAdd(fixtureLocation)} - > - - - + handleClickAdd(fixtureLocation)} + > + + ) } const EMPTY_CONFIG_STYLE = css` + display: ${DISPLAY_FLEX}; align-items: ${ALIGN_CENTER}; justify-content: ${JUSTIFY_CENTER}; background-color: ${COLORS.mediumBlueEnabled}; diff --git a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx index 80cecfd71d5..6f01125d965 100644 --- a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { css } from 'styled-components' import { Icon } from '../../icons' -import { Btn, Flex, Text } from '../../primitives' +import { Btn, Text } from '../../primitives' import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles' import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' @@ -44,25 +44,26 @@ export function StagingAreaConfigFixture( flexProps={{ flex: '1' }} foreignObjectProps={{ flex: '1' }} > - + handleClickRemove(fixtureLocation) + : () => {} + } + > {STAGING_AREA_DISPLAY_NAME} - {handleClickRemove != null ? ( - handleClickRemove(fixtureLocation)} - > - - - ) : null} - + + ) } const STAGING_AREA_CONFIG_STYLE = css` + display: ${DISPLAY_FLEX}; align-items: ${ALIGN_CENTER}; background-color: ${COLORS.grey2}; border-radius: ${BORDERS.borderRadiusSize1}; diff --git a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx index c0c745e367e..03879379c98 100644 --- a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { css } from 'styled-components' import { Icon } from '../../icons' -import { Btn, Flex, Text } from '../../primitives' +import { Btn, Text } from '../../primitives' import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles' import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' @@ -50,25 +50,26 @@ export function TrashBinConfigFixture( flexProps={{ flex: '1' }} foreignObjectProps={{ flex: '1' }} > - + handleClickRemove(fixtureLocation) + : () => {} + } + > {TRASH_BIN_DISPLAY_NAME} - {handleClickRemove != null ? ( - handleClickRemove(fixtureLocation)} - > - - - ) : null} - + + ) } const TRASH_BIN_CONFIG_STYLE = css` + display: ${DISPLAY_FLEX}; align-items: ${ALIGN_CENTER}; background-color: ${COLORS.grey2}; border-radius: ${BORDERS.borderRadiusSize1}; diff --git a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx index 5e974d7eb8a..c4163f8c7a3 100644 --- a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { css } from 'styled-components' import { Icon } from '../../icons' -import { Btn, Flex, Text } from '../../primitives' +import { Btn, Text } from '../../primitives' import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles' import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' @@ -53,25 +53,26 @@ export function WasteChuteConfigFixture( flexProps={{ flex: '1' }} foreignObjectProps={{ flex: '1' }} > - + handleClickRemove(fixtureLocation) + : () => {} + } + > {WASTE_CHUTE_DISPLAY_NAME} - {handleClickRemove != null ? ( - handleClickRemove(fixtureLocation)} - > - - - ) : null} - + + ) } const WASTE_CHUTE_CONFIG_STYLE = css` + display: ${DISPLAY_FLEX}; align-items: ${ALIGN_CENTER}; background-color: ${COLORS.grey2}; border-radius: ${BORDERS.borderRadiusSize1}; From 8bd98aac5767eda18e46bfa96f91fa111346610e Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 21 Nov 2023 10:59:26 -0500 Subject: [PATCH 09/11] refactor(robot-server,api-client): Rename `GET /deck_configuration` field `lastUpdatedAt` to `lastModifiedAt` (#14029) --- api-client/src/deck_configuration/types.ts | 2 +- .../robot_server/deck_configuration/models.py | 2 +- .../robot_server/deck_configuration/router.py | 4 ++-- .../robot_server/deck_configuration/store.py | 8 ++++---- .../http_api/test_deck_configuration.tavern.yaml | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/api-client/src/deck_configuration/types.ts b/api-client/src/deck_configuration/types.ts index 6396df3aeaf..8ed7db78658 100644 --- a/api-client/src/deck_configuration/types.ts +++ b/api-client/src/deck_configuration/types.ts @@ -9,6 +9,6 @@ export interface UpdateDeckConfigurationRequest { export interface DeckConfigurationResponse { data: { cutoutFixtures: DeckConfiguration - lastUpdatedAt: string + lastModifiedAt: string } } diff --git a/robot-server/robot_server/deck_configuration/models.py b/robot-server/robot_server/deck_configuration/models.py index a0f2b6b7114..51130ce88ba 100644 --- a/robot-server/robot_server/deck_configuration/models.py +++ b/robot-server/robot_server/deck_configuration/models.py @@ -49,7 +49,7 @@ class DeckConfigurationResponse(pydantic.BaseModel): cutoutFixtures: List[CutoutFixture] = pydantic.Field( description="A full list of all the cutout fixtures that are mounted onto the deck." ) - lastUpdatedAt: Optional[datetime] = pydantic.Field( + lastModifiedAt: Optional[datetime] = pydantic.Field( description=( "When the deck configuration was last set over HTTP." " If that has never happened, this will be `null` or omitted." diff --git a/robot-server/robot_server/deck_configuration/router.py b/robot-server/robot_server/deck_configuration/router.py index af0b7e80498..6b1a3fc33c8 100644 --- a/robot-server/robot_server/deck_configuration/router.py +++ b/robot-server/robot_server/deck_configuration/router.py @@ -59,7 +59,7 @@ async def put_deck_configuration( # noqa: D103 request_body: RequestModel[models.DeckConfigurationRequest], store: DeckConfigurationStore = fastapi.Depends(get_deck_configuration_store), - last_updated_at: datetime = fastapi.Depends(get_current_time), + now: datetime = fastapi.Depends(get_current_time), deck_definition: DeckDefinitionV4 = fastapi.Depends(get_deck_definition), ) -> PydanticResponse[ Union[ @@ -70,7 +70,7 @@ async def put_deck_configuration( # noqa: D103 placements = validation_mapping.map_in(request_body.data) validation_errors = validation.get_configuration_errors(deck_definition, placements) if len(validation_errors) == 0: - success_data = await store.set(request_body.data, last_updated_at) + success_data = await store.set(request=request_body.data, last_modified_at=now) return await PydanticResponse.create( content=SimpleBody.construct(data=success_data) ) diff --git a/robot-server/robot_server/deck_configuration/store.py b/robot-server/robot_server/deck_configuration/store.py index 51f9f9c5281..b654dac9ad7 100644 --- a/robot-server/robot_server/deck_configuration/store.py +++ b/robot-server/robot_server/deck_configuration/store.py @@ -20,11 +20,11 @@ def __init__(self, deck_type: DeckType) -> None: self._last_update: Optional[_LastUpdate] = None async def set( - self, request: models.DeckConfigurationRequest, last_updated_at: datetime + self, request: models.DeckConfigurationRequest, last_modified_at: datetime ) -> models.DeckConfigurationResponse: """Set the robot's current deck configuration.""" self._last_update = _LastUpdate( - cutout_fixtures=request.cutoutFixtures, timestamp=last_updated_at + cutout_fixtures=request.cutoutFixtures, timestamp=last_modified_at ) return await self.get() @@ -35,12 +35,12 @@ async def get(self) -> models.DeckConfigurationResponse: cutoutFixtures=defaults.for_deck_definition( self._deck_type.value ).cutoutFixtures, - lastUpdatedAt=None, + lastModifiedAt=None, ) else: return models.DeckConfigurationResponse.construct( cutoutFixtures=self._last_update.cutout_fixtures, - lastUpdatedAt=self._last_update.timestamp, + lastModifiedAt=self._last_update.timestamp, ) async def get_deck_configuration(self) -> DeckConfigurationType: diff --git a/robot-server/tests/integration/http_api/test_deck_configuration.tavern.yaml b/robot-server/tests/integration/http_api/test_deck_configuration.tavern.yaml index 59253102b8c..87fedcd1f18 100644 --- a/robot-server/tests/integration/http_api/test_deck_configuration.tavern.yaml +++ b/robot-server/tests/integration/http_api/test_deck_configuration.tavern.yaml @@ -12,14 +12,14 @@ stages: response: json: data: - # lastUpdatedAt is deliberately omitted from this expected object. - # A lastUpdatedAt that's omitted or null means the deck configuration has never been set. + # lastModifiedAt is deliberately omitted from this expected object. + # A lastModifiedAt that's omitted or null means the deck configuration has never been set. # # Unfortunately, this makes this test order-dependent with any other tests # that modify the deck configuration, even if they try to restore the original value # after they're done. We probably need some kind of deck configuration factory-reset. # - # lastUpdatedAt: null + # lastModifiedAt: null cutoutFixtures: - cutoutFixtureId: singleLeftSlot cutoutId: cutoutA1 @@ -86,11 +86,11 @@ stages: response: json: data: - lastUpdatedAt: !anystr + lastModifiedAt: !anystr cutoutFixtures: *expectedCutoutFixtures save: json: - last_updated_at: data.lastUpdatedAt + last_modified_at: data.lastModifiedAt - name: Set an invalid deck configuration request: @@ -142,5 +142,5 @@ stages: response: json: data: - lastUpdatedAt: '{last_updated_at}' + lastModifiedAt: '{last_modified_at}' cutoutFixtures: *expectedCutoutFixtures From b948d8188cea6671ddc261b04c545f06901cfd68 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 21 Nov 2023 14:48:27 -0500 Subject: [PATCH 10/11] refactor(protocol-engine): Delete duplicate DeckConfiguration type (#14035) --- .../opentrons/protocol_engine/state/addressable_areas.py | 8 ++------ api/src/opentrons/protocol_engine/types.py | 2 +- .../protocol_engine/state/test_addressable_area_store.py | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py index 04abca05a2e..fb507ebca08 100644 --- a/api/src/opentrons/protocol_engine/state/addressable_areas.py +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -1,6 +1,6 @@ """Basic addressable area data state and store.""" from dataclasses import dataclass -from typing import Dict, Set, Union, List, Tuple +from typing import Dict, Set, Union, List from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 @@ -59,10 +59,6 @@ def _get_conflicting_addressable_areas( return loaded_areas_on_cutout -# TODO make the below some sort of better type -DeckConfiguration = List[Tuple[str, str]] # cutout_id, cutout_fixture_id - - class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions): """Addressable area state container.""" @@ -126,7 +122,7 @@ def _handle_command(self, command: Command) -> None: @staticmethod def _get_addressable_areas_from_deck_configuration( - deck_config: DeckConfiguration, deck_definition: DeckDefinitionV4 + deck_config: DeckConfigurationType, deck_definition: DeckDefinitionV4 ) -> Dict[str, AddressableArea]: """Load all provided addressable areas with a valid deck configuration.""" # TODO uncomment once execute is hooked up with this properly diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 0ddd8ae96c0..60283f66885 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -776,4 +776,4 @@ class QuadrantNozzleLayoutConfiguration(BaseModel): ] # TODO make the below some sort of better type -DeckConfigurationType = List[Tuple[str, str]] +DeckConfigurationType = List[Tuple[str, str]] # cutout_id, cutout_fixture_id diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py index 018332293b4..d4b35f32ae3 100644 --- a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -16,9 +16,9 @@ from opentrons.protocol_engine.state.addressable_areas import ( AddressableAreaStore, AddressableAreaState, - DeckConfiguration, ) from opentrons.protocol_engine.types import ( + DeckConfigurationType, DeckType, ModuleModel, LabwareMovementStrategy, @@ -33,7 +33,7 @@ ) -def _make_deck_config() -> DeckConfiguration: +def _make_deck_config() -> DeckConfigurationType: return [ ("cutoutA1", "singleLeftSlot"), ("cutoutB1", "singleLeftSlot"), From a582169f538035d697e7c6ec4c71774aec960f3d Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Tue, 21 Nov 2023 14:49:12 -0500 Subject: [PATCH 11/11] docs(robot-server): Update GET /health response examples (#14005) --- robot-server/robot_server/health/models.py | 45 +++++++++------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/robot-server/robot_server/health/models.py b/robot-server/robot_server/health/models.py index 21f0e6e547e..16090ade4d5 100644 --- a/robot-server/robot_server/health/models.py +++ b/robot-server/robot_server/health/models.py @@ -11,26 +11,35 @@ class HealthLinks(BaseModel): apiLog: str = Field( ..., description="The path to the API logs endpoint", + examples=["/logs/api.log"], ) serialLog: str = Field( ..., description="The path to the motor control serial communication logs endpoint", + examples=["/logs/serial.log"], ) serverLog: str = Field( ..., description="The path to the HTTP server logs endpoint", + examples=["/logs/server.log"], ) oddLog: typing.Optional[str] = Field( None, - description="The path to the ODD app logs endpoint", + description=( + "The path to the on-device display app logs endpoint" + " (only present on the Opentrons Flex)" + ), + examples=["/logs/touchscreen.log"], ) apiSpec: str = Field( ..., description="The path to the OpenAPI specification of the server", + examples=["/openapi.json"], ) systemTime: str = Field( ..., description="The path to the system time endpoints", + examples=["/system/time"], ) @@ -42,6 +51,7 @@ class Health(BaseResponseBody): description="The robot's name. In most cases the same as its " "mDNS advertisement domain name, but this can get out " "of sync. Mostly useful for user-facing titles.", + examples=["Otie"], ) robot_model: RobotModel = Field( ..., @@ -50,23 +60,26 @@ class Health(BaseResponseBody): api_version: str = Field( ..., description="The API server's software version", + examples=["3.15.2"], ) fw_version: str = Field( ..., description="The motor controller's firmware version. Doesn't " "follow a pattern; suitable only for display or exact matching.", + examples=["v2.15.0"], ) board_revision: str = Field( ..., description="The hardware revision of the OT-2's central routing board.", + examples=["2.1"], ) logs: typing.List[str] = Field( ..., description="List of paths at which to find log endpoints", + examples=[["/logs/serial.log", "/logs/api.log"]], ) system_version: str = Field( - ..., - description="The robot's operating system version.", + ..., description="The robot's operating system version.", examples=["1.2.1"] ) maximum_protocol_api_version: typing.List[int] = Field( ..., @@ -74,6 +87,7 @@ class Health(BaseResponseBody): "in the format `[major_version, minor_version]`", min_items=2, max_items=2, + examples=[[2, 8]], ) minimum_protocol_api_version: typing.List[int] = Field( ..., @@ -81,32 +95,11 @@ class Health(BaseResponseBody): "in the format `[major_version, minor_version]`", min_items=2, max_items=2, + examples=[[2, 0]], ) robot_serial: typing.Optional[str] = Field( default=None, description="The robot serial number. Should be used if present; if not present, use result of /server/update/health.", + examples=["OT2CEP20190604A02"], ) links: HealthLinks - - class Config: - """Health response model schema configuration.""" - - schema_extra = { - "example": { - "name": "OT2CEP20190604A02", - "api_version": "3.15.2", - "fw_version": "v2.15.0", - "board_revision": "2.1", - "logs": ["/logs/serial.log", "/logs/api.log"], - "system_version": "1.2.1", - "maximum_protocol_api_version": [2, 8], - "minimum_protocol_api_version": [2, 0], - "links": { - "apiLog": "/logs/api.log", - "serialLog": "/logs/serial.log", - "apiSpec": "/openapi.json", - "systemTime": "/system/time", - }, - "robot_serial": None, - } - }