From 19e6d6e9f15719961636484613bd43a8b6558269 Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Wed, 5 Jul 2023 16:00:17 +0200 Subject: [PATCH] #98: Added more robust connection protocol (#108) * Introduced a tcp-like protocol with a two-side synchronize and ack. When a peer receives synchronize it can finish for its peer, but needs to wait, if it receives an ack it can finish immediately for the peer. * Also added a log analyzer * Install itde as python dependency and pin it to 1.7.1 Co-authored-by: Nicola Coretti --- .gitignore | 4 + .../establish_connection/sequence/base.puml | 15 + .../both_peers_receive_register_peer.png | Bin 0 -> 44476 bytes .../both_peers_receive_register_peer.puml | 25 + ...gister_peer_both_peers_lose_synchonize.png | Bin 0 -> 52090 bytes ...ister_peer_both_peers_lose_synchonize.puml | 32 + ...egister_peer_one_peer_loses_synchonize.png | Bin 0 -> 50024 bytes ...gister_peer_one_peer_loses_synchonize.puml | 28 + .../one_peer_receives_register_peer.png | Bin 0 -> 37619 bytes .../one_peer_receives_register_peer.puml | 18 + .../establish_connection/state_machine.png | Bin 0 -> 48928 bytes .../establish_connection/state_machine.puml | 18 + doc/design/legend.puml | 14 + .../both_peer_receive_ping.png | Bin 0 -> 38597 bytes .../both_peer_receive_ping.puml | 36 + .../one_peer_receives_ping.png | Bin 0 -> 31576 bytes .../one_peer_receives_ping.puml | 31 + .../udf_discovery_and_communication.rst | 89 +- .../udf_communication/ip_address.py | 2 - .../local_discovery_socket.py | 2 - .../udf_communication/messages.py | 21 +- .../peer_communicator/abort_timeout_sender.py | 51 ++ .../background_listener_interface.py | 32 +- .../background_listener_thread.py | 87 +- .../background_peer_state.py | 165 ++-- .../peer_communicator/clock.py | 7 + .../get_peer_receive_socket_name.py | 2 +- .../peer_communicator/peer_communicator.py | 54 +- .../peer_communicator/peer_is_ready_sender.py | 63 ++ .../peer_communicator/sender.py | 49 ++ .../synchronize_connection_sender.py | 46 + .../peer_communicator/timer.py | 19 + .../socket_factory/fault_injection.py | 2 - poetry.lock | 789 ++++++++++++++---- pyproject.toml | 1 + scripts/start_integration_test_environment.sh | 17 +- .../peer_communication/analyze_log.py | 117 +++ .../conditional_method_dropper.py | 12 + .../peer_communication/mock_cast.py | 6 + .../test_abort_timeout_sender.py | 168 ++++ .../peer_communication/test_add_peer.py | 65 +- .../test_background_peer_state.py | 149 ++++ .../test_peer_is_ready_sender.py | 170 ++++ .../peer_communication/test_send_recv.py | 54 +- .../test_synchronize_connection_sender.py | 179 ++++ .../peer_communication/test_timer.py | 82 ++ .../peer_communication/utils.py | 10 +- .../socket_factory/zmq/test_zmq_socket.py | 2 +- .../udf_communication/test_local_discovery.py | 49 +- tests/udf_communication/test_messages.py | 5 +- 50 files changed, 2325 insertions(+), 462 deletions(-) create mode 100644 doc/design/establish_connection/sequence/base.puml create mode 100644 doc/design/establish_connection/sequence/both_peers_receive_register_peer.png create mode 100644 doc/design/establish_connection/sequence/both_peers_receive_register_peer.puml create mode 100644 doc/design/establish_connection/sequence/both_peers_receive_register_peer_both_peers_lose_synchonize.png create mode 100644 doc/design/establish_connection/sequence/both_peers_receive_register_peer_both_peers_lose_synchonize.puml create mode 100644 doc/design/establish_connection/sequence/both_peers_receive_register_peer_one_peer_loses_synchonize.png create mode 100644 doc/design/establish_connection/sequence/both_peers_receive_register_peer_one_peer_loses_synchonize.puml create mode 100644 doc/design/establish_connection/sequence/one_peer_receives_register_peer.png create mode 100644 doc/design/establish_connection/sequence/one_peer_receives_register_peer.puml create mode 100644 doc/design/establish_connection/state_machine.png create mode 100644 doc/design/establish_connection/state_machine.puml create mode 100644 doc/design/legend.puml create mode 100644 doc/design/local_discovery_strategy/both_peer_receive_ping.png create mode 100644 doc/design/local_discovery_strategy/both_peer_receive_ping.puml create mode 100644 doc/design/local_discovery_strategy/one_peer_receives_ping.png create mode 100644 doc/design/local_discovery_strategy/one_peer_receives_ping.puml create mode 100644 exasol_advanced_analytics_framework/udf_communication/peer_communicator/abort_timeout_sender.py create mode 100644 exasol_advanced_analytics_framework/udf_communication/peer_communicator/clock.py create mode 100644 exasol_advanced_analytics_framework/udf_communication/peer_communicator/peer_is_ready_sender.py create mode 100644 exasol_advanced_analytics_framework/udf_communication/peer_communicator/sender.py create mode 100644 exasol_advanced_analytics_framework/udf_communication/peer_communicator/synchronize_connection_sender.py create mode 100644 exasol_advanced_analytics_framework/udf_communication/peer_communicator/timer.py create mode 100644 tests/udf_communication/peer_communication/analyze_log.py create mode 100644 tests/udf_communication/peer_communication/conditional_method_dropper.py create mode 100644 tests/udf_communication/peer_communication/mock_cast.py create mode 100644 tests/udf_communication/peer_communication/test_abort_timeout_sender.py create mode 100644 tests/udf_communication/peer_communication/test_background_peer_state.py create mode 100644 tests/udf_communication/peer_communication/test_peer_is_ready_sender.py create mode 100644 tests/udf_communication/peer_communication/test_synchronize_connection_sender.py create mode 100644 tests/udf_communication/peer_communication/test_timer.py diff --git a/.gitignore b/.gitignore index f2eac516..e660e491 100644 --- a/.gitignore +++ b/.gitignore @@ -148,6 +148,10 @@ doc/_build .luarocks +# vim +.*.swp +.*.swo + # vagrant .vagrant diff --git a/doc/design/establish_connection/sequence/base.puml b/doc/design/establish_connection/sequence/base.puml new file mode 100644 index 00000000..9f74961c --- /dev/null +++ b/doc/design/establish_connection/sequence/base.puml @@ -0,0 +1,15 @@ +@startuml + +!include ../../legend.puml + +box "Peer1" + participant "Frontend" as Peer1Frontend + participant "Backend" as Peer1Backend +end box + +box "Peer2" + participant "Backend" as Peer2Backend + participant "Frontend" as Peer2Frontend +end box + +@enduml \ No newline at end of file diff --git a/doc/design/establish_connection/sequence/both_peers_receive_register_peer.png b/doc/design/establish_connection/sequence/both_peers_receive_register_peer.png new file mode 100644 index 0000000000000000000000000000000000000000..6d2de9524b33feaf0c84387f5c57f2bf1a1a8aea GIT binary patch literal 44476 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⋙PYepzhdd%$I(!t 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 Peer1Frontend: register_peer() + Peer1Frontend -[$zmq_inproc]>> Peer1Backend: RegisterPeerMessage + Peer1Backend -[$zmq_tcp_rd_no_con]>> Peer2Backend: SynchronizeConnectionMessage +else + Peer2Frontend <[$method_call]-] : register_peer() + Peer2Backend <<[$zmq_inproc]- Peer2Frontend: RegisterPeerMessage + Peer1Backend <<[$zmq_tcp_rd_no_con]- Peer2Backend : SynchronizeConnectionMessage +end + +par + Peer1Backend <<[$zmq_tcp_rd_no_con]- Peer2Backend: AcknowledgeConnectionMessage + Peer1Backend -[$zmq_inproc]>> Peer1Frontend: PeerIsReadyMessage(Peer2) +else + Peer1Backend -[$zmq_tcp_rd_no_con]>> Peer2Backend: AcknowledgeConnectionMessage + Peer2Backend -[$zmq_inproc]>> Peer2Frontend: PeerIsReadyMessage(Peer1) +end + +@enduml diff --git a/doc/design/establish_connection/sequence/both_peers_receive_register_peer_both_peers_lose_synchonize.png b/doc/design/establish_connection/sequence/both_peers_receive_register_peer_both_peers_lose_synchonize.png new file mode 100644 index 0000000000000000000000000000000000000000..de0c40a810b87ddd359d80c8d9aabc7b0b5519f8 GIT binary patch literal 52090 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 literal 0 HcmV?d00001 diff --git a/doc/design/establish_connection/sequence/both_peers_receive_register_peer_both_peers_lose_synchonize.puml b/doc/design/establish_connection/sequence/both_peers_receive_register_peer_both_peers_lose_synchonize.puml new file mode 100644 index 00000000..ae093cd4 --- /dev/null +++ b/doc/design/establish_connection/sequence/both_peers_receive_register_peer_both_peers_lose_synchonize.puml @@ -0,0 +1,32 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include base.puml + +par + [-[$method_call]> Peer1Frontend: register_peer() + Peer1Frontend -[$zmq_inproc]>> Peer1Backend: RegisterPeerMessage + Peer1Backend -[$zmq_tcp_rd_no_con]x Peer2Backend: SynchronizeConnectionMessage +else + Peer2Frontend <[$method_call]-] : register_peer() + Peer2Backend <<[$zmq_inproc]- Peer2Frontend: RegisterPeerMessage + Peer1Backend x[$zmq_tcp_rd_no_con]- Peer2Backend : SynchronizeConnectionMessage +end + +loop until abort timeout + par + ...synchronize_timeout... + Peer1Backend -[$zmq_tcp_rd_no_con]x Peer2Backend: SynchronizeConnectionMessage + else + ...synchronize_timeout... + Peer1Backend x[$zmq_tcp_rd_no_con]- Peer2Backend : SynchronizeConnectionMessage + end +end + +par + Peer2Backend -[$zmq_inproc]>> Peer2Frontend: TimeoutMessage +else + Peer1Frontend <<[$zmq_inproc]- Peer1Backend: TimeoutMessage +end + +@enduml \ No newline at end of file diff --git a/doc/design/establish_connection/sequence/both_peers_receive_register_peer_one_peer_loses_synchonize.png b/doc/design/establish_connection/sequence/both_peers_receive_register_peer_one_peer_loses_synchonize.png new file mode 100644 index 0000000000000000000000000000000000000000..5030bb80c679fcb417a513356375d8c88f0e38bd GIT binary patch literal 50024 zcmd43cRbep`#w%WnW>aLQdyzwh$|vdnc1sURz^g&OCcef6j|9jA$wGWWUt5`S=nS? zuits0?(x3w&-e5B{(j%b_t!t}hx_unp0DRP&*MCf<9vE6%FB=v(GcO_;E>8*ky6IN z!BfS-+1GJkAN(d+OJ^H?aM(y|*xa_f?_hS<&<025uEkyJTQ+y^Fc~;78Qa+0e;~rc zbKmTig^jJb8TV~V^M{R}8Q~F?CO0%}e*GQ?7oKDPszL3#WzWS!Wt-~POcFof9!T&z zK`brHlqx5Yc+uMUsHU%&t(xYXNXflLW>;rBKR+wEaz(QI!n1svhW(K%A>?7Fm02ks zi0hlsa~5Df-Sn ziu&G_O9yX<6P}d}%~WA8yv$I@bS^;m+SaV=YmrA6_!P0 zseD(j@JK#C78TpZZj}~(PQPx_;z+V%QOng=_KF#4`=b@s=!%JDFapO5NZjw^t*KnK z&#Qf$zrCsJ7qows`m%tzI%l4dWL4n(>(mLT7Fx#ktIxFsH^$xjO?BDlD5h_XX)H=@Q=>Hk;veCMt)DRTlEfgE1<6 zw>)%2x0h}yy;)6ky(Vx?F+yq3d185BkN7+H=qpCgSq<3zeJKeYcqrY3X$Dzke5>j+` zrT7;F+_IVQJ`V4{LxGDg)oru?vYre@3-z(^vlkDrF>zdVdvooY+exBsrh~WWwpa$( zMA%+`eEQw#OVHbtEV9&-9m$*LS69**)nc+lRHMo_XF0D6bf&|X!#@tCCgI)x(!fLT zu>YkeVL!^w9)YAdC)N3j#D&gF^A`Xjp z)?!$dat~3v6)&B%)w3%us{i)R{9D{5!tDcZ4hl?@VF;(FIHXJqypOPAFsl`oU0L*D zH2He6vfgczW&`EhahJTTt*ukK=*VQOta7GrnwYRto|qA^ooZ(ZdulR-X1LrSc?V~z zG34tFyQ-;&h}E%&^T_dbfK49WS*3`(WhwI z&7%}+CJ;%f+kM5hHy+~R;$F8oqf^*yJ72ciA20dL*O%`L4zhICba(Np{EOj*Y3mqe zhx*H$Rz@ntclKAx?Q6sfAIVV}`V|-iGl%HezA0UNOtltcv%4=Z$VPSb!C2w7H-F9` zFf-Hd_Wj)3inq0%R*9|lURn{lS?MMla+;K0B+qGec9viW-S76tVmcw)tL~2qJ+>n}j<%7w6{VoyYGUq(L+e&XNDwF~Y$)y?aD(Z&L0oKF(Q*o%JKbN#B`T9JhnJ zBf!y+ZYWl1LFDA*j&W_Y{pf!1f=dA%;UbShasYNVia;Zu(vt5s?yfh0rN-B?*Nt7ygGu5Z6g zJe4IWfo!(LU;^&NnyinbknMJr2YH0VKivQA!;xZ6(f5uDbLAQ}bupZIL;hT%)k_BZ z7MsPFsvQWP54G^5h~jTsmfs^2=3|SpW8JHfm$yNY;peUfN5On~WU?gDWZ!+b)5M)qbSAfTzngH=WyhK(hmavqq+9 zj6D0BfGH1|%E2;aY_QudR!o{J7riyt|Iwhk0@Y+28Nnqhi*TN5s}xToJ6slNcy~<2 zqcii{;p1ImBE=!ohU&Vb<+*9FO;TL@&Rv10rG5PP1n9g?3;*< zy~l{&Vy9u}L7aOFWhRBbfAXWPsafrRXI}AcOdpAm45K-Ew7t~9*QCwR+lnafr;{?A z&DPqw5dCJ7xVVO_qVEw^RgYx+L)FFbxbX2~nxET9(Zrm8z#V35%S-wrenY}>EKJpp zM&R)DTv@o)H*-6Fa~c{tI_oB}Rmx)+m1i-+3)wOZFRv+Be0<_JVJ^%%zQ*1sI$alH zwE43vglIKHp?p!p9((HAGiIIn6XC@f8FJQ^of5Gjdehb_d~$w1g@T2lzx4cebDRMU zwYc-O8vh~7iZU_L5;s?sIG=XMvnQ(-#!(fQN6_2TrYKa!%5;H`tVKmx48|@Zh$Y0b zc=EfL6PcmW@qWkY8Jp~fXYBY$X!!Y0oQTjvZT=iyupf{bn1k783i(`z$;~^ zEn3@T@}(cueYs#g;un>FgM?NvSTZbPV|KQWa574R#B?r$Uwd^{?CMpdLWO}O03wMS zO?%W_($TLn0w3eK#JBZQclQh%=gM@~Y6;0UnN=FE5hTlwckJ94G%oHV%km;U+R5JY zYFmpE+GGqDZqQb&f7i=36l3XJc`5F?m0MZ)MEl*3!$KyWu3hVEBS%!f-2d^_L?ou5 zU+&0Ab#+YZ+(1ViKWqc{(Yd*S1lf?wel#s1n<#|uMYKd$!)b+RZr2*hILGNs(kG;B z?-9=dNcMdZSle$Ebn3Io~1cOxP!y0Xl2=q_m3zA(d#Xrlp9LTYq z7F_=JX3bVjN^&{EHSR+5C!4P6>|`Gbg(ujEba7?cUqbGY%ME%4!?`eQ2%qb!?lbDy zScZ5SIDBe2Yew34`uQy1_EP$A*(xs?JxAyB5Y~zmgD1<&XL`os1LX2cUca`;kWYe# zpss`-bKL06>U$OPU@PKY)=PnVuA1FVXc@t~pTE@4WuRmvOY)?(*~ra%@=6d4;fWKk ztM$_zcrRchx~ZC-c!V;Xc=7-Z=Z#+{5H`{BrWNe;W%R+r^nE6uo}JIqp5k+jVM*IzO;a-Mux zYC>bQpOMq>-zd#Jb_~-tFo0O1oodc1B!A{ipAcJlLVH5SkH+b9?oKLD%q?DO!LF#Q zebsQ=uUSeId6{&#HKArwNOvRA!&NC-WTg(GL-kzM>|xpZrxfINlZRzUIbC_E&+`^J zEFI%i&uaMmdEo8c@OSEzd?!zB^YoRtY!2}81vHy2r={$Sl0L!1vX30Mr}&x;N>%sN51)!E$#FOPkGA zmlNzILVY%T+q^adF7EQGuE{Qoqmx|P?}!c@FzZOm|2ZvB$#;}8>a~6qJsE2mm0^S9 zcQnT6{@yn1fTQ?RwxVUWCyZ-0g7@xeu@Nq9zCJtK*$q@p?n{Wyli2wQQ{C}CD3jZ&r+Oe&HrRBtxq5pv9a5-awfWwTC?nAB*u_g0zcE0d5UHhMJajT+LNy{P7a%|BOCMK#&+%>rqe zu}IE{$C{6niiuMad)O1&qcTGG3D)`rVv|zt@uIH0eFXnFl>Z?hbVqP;t|{)66L^&z zdqdJ|Bl_1b9xx#!38ZAko)_gQc{Mp)+R@8buu!7gxAz*}Kac5$v_%pw-W};H5JBBS zi$lVcD+!8Vq5vQLJR#KPhWqwJ0u+lE<@)O6*l)V`!bAb4atd;~&iPN%*2jYhkBLDA z;S(R{cCPbunk_OC#Hl1&PCHi~=*Ht#3@`F(YPLO9Mcp)7+jRlOo9u`!VR0&r9M;Fe zv)eBe=nSE~y}gNDo{x^+aXb)m`nqGcr-z5fc^n+h=JZc%lKaYWrT{?Cr<6b~8nMZ! z*{&{L-=nzc+$C087g`%7E;{?4E3an^6z{S09Nq`9JlEsPQe!##iTQ1C@BD4x`>%6{L?R(tI!((}>fO1sjY?(zT++mhFB2 z@EBj;HVZ+M)&!^#lNDmh$DV5&rvb1nFmCzK)Wjn$u4i^_6;u2mN;!W^HTDqh8?pe& z8;7uL$&ivA!%9V9U?4!6W=BKA(=+1s^Mj$GN3~uGn)h_)8da`ix-HLBz^~hLjlE0DM$-9%*o614-BQG_GelGE%NaRX7P?>BGW zoW35>YgI$-aq6z5mT#$E;gEW?8wn0!-}-l;tcsrvokv}T zw@N*PzZcs6{#GM%Xtl9yNo8iD(SLdG^f2TUE34&-MHOn%Ocuj&lJW$OIO(6JH-8Zo zQVF7k((C_4MVX85$_C4|| zaz2$|=6L!$at%M&9_y2rJ?%!uDm&5wq+5iNvrYKP(y3En$xJ&r> z_@9&_i%gl1QBvAYG(TD_fMZZtSoroW@5nJ6HUgN=!1PXfoJyR4;+;R&r-W*SO+cVS z*pyG5t19Pq4U`HR8LKnj?D|U`)ceWZtg#2J$g~d^hh#oQH2uVQC!$qgu|4F)_;a$2 z_3YXC+$M2R*sm_DeO-AbZ4YO9xO9tem_2}#rfpd!0KC}g(}uwRBCk=D)| z&-?O}PjsY*zenJWI&yBy^h8}Y=2;N>&{tyD1%OW^bEvuJ6HT7PK)FlB)?&Tyah_n6 z-~++;)Cynv5kUCR)zXRx3ZgiCSaMiHZxT&GK{1-KzP`T9N<9Sxo)j4%@~y3{pD@wV zA~=d9*c~*F!pVSqqa&SVnzmR0q1$_9q$WVfxcTA>x^vc}wK5D232)wbGZC@RR9oJV~(LDA%3HW*qWMevex+_~BCQ&4k>Xn$&J9~dZ zQW}1S$UL2l6e~ zC_p2b@@>lZa4V6hk6z?WwY3L`iQC={6j;W*M~o9=V{bw*hktF!3KjMX27eya`z+m( zyhvtdW*gzQ?y8%Lii){%#`Tbj7v(-`=NkIi563t!)&a3Q8Q+|$l6>mw^9dQ#XT5WI zSp2aYbufGI45H5NM4^W6OIcY#Wkl*(X+kIZ@Cy_%n{yS%g@i16C#KMkVHl7Pmb#5% z5?2_+9F1X)%suO8vziGk%h!`Mki~kQ$uGpj#Mh0ExcA7o(2SxXavquXJ;qf$nH0Ia z`p&F-F|(k*HK3iU=#SGrj~eu?^@Di*fU+np^*N$?Lr+@BYFNrjqQ5SLRUt}5np3yf z#$hTonHzr2_7;7!tmqp=!|TavYEt)Y!TEVYM!!*YM4@@Z4HwVw%L@t49!jStCi#6l zjNSWt2A^wNoOk+phTgHa(5j`SWtdl@t$UcWuBi!SHC$EhvdPQGSD(FHA#ksg|6b>{ z5=?eVN^MhKqn9C8Vozn^3M##HwckleN$Hc46BF_c_L5b*7(^u0))4sy8d2CRzcTaZoe2@C z`_V3PhDCr_mcLlBy}9bEuu=|*7AD)}gU7*7T&6Vu0!sAi-rV}gk+y&v_bB}s5W2U# z$mj(v`%A6IHESFg61O5 zYxnJtrUH@V#_epwjC!r(c}x>O!wQn`CR5MhRn`mvKr~S4!>y!Zu!S^YI)*NP?BZE? z`kvnapX7)u#dPc&1g4nOh$)3d-B-(k6`HCJ%BC55Ti^>-(jJEehlUCL| zawEhBF|$lj91RE$89sDd*<{T}70-|&Ullin6=wx9e_?>C*jv!YGkjPv2TyMiyT36AWPn|W0vpcl{Ov_AkLax z&G^sgv6) z=sy7hcmuoI-8uju3O0eJQ|9)^j3Lj^?(3v^$d6wf5a(ldNnz#jw{J);OQ(gTBroyiC?<(@|i2N5g`R9UU=fP0^Vb}OEwrbh~9nO)pwY8+Qf>biN zDFWD`QgzYe2)^H)8wHvjtqnwy>&3RykdA3aj&*fT{WH*Q+Q_-J@}3gZ_|Yaa?(fGUjYD%8qBy10J4av;-O3~s zA?75Q=?nm%E$KQ7@t3A1A&VQtv*nu>j*@=ys;{GzEi7iMypOoMy91nmMyvVg^pBqW z=@g~-W%rXM5Ci8?JYHD1WCsYQpJeyApqx^NS@^HNdsD}j5*qCNp| zsv>}>1)^M^JGpqudd&2NEb0Jv$9m}D~$v_r9V#jg>mO4X|U^O`DG z+w}-^pJ<~&xjKWM6~Ks+7aAEJz8t{t8&L!Y2TQsGpg4Jv|3zfvw@hszcO+AbJy*B<&K3n>mQZabCUy}o4P#vtUJ1uVwGMsss%x4EHFmPwj36tPFSMC#)wU?| z#2ihQFc1L*_uifG9%fu5VEUsKeOwrzq@?8Jw$CMV< z;#fUBnT(#Ep2Jv}DHQpKx|$#?@bK}4sJ(vu`t-FIidtHZqqP)o+1wF&;rmQ1EbbPq z1VGBv)z#(o7XTA~yXLvNYt#oDEpxkbsztO2r(JILWbCxZ2c+L&oZit5$kwBb4@?24 z?QBKOVjB~nE~?Bp)iPq2gAvsQcWW^ps#Vvtr-7N+?D-$HRl==6uHY~)2FIsrP|dsu z3_L_fM+b+Wo}NpOvczsKi4>%SYu8A&qwmaDz_)pLd4=4}htmW2=KT5dHtMFPrf~F3 z6scZJ=4BNdPN9=_PE3jm0W}51Ig{3tIa#ZyE(aY(|LH4MRtuGn@Fu@s7wVIWaQbFm z5QK!4AQD9k($Dbhu)|wuga{l7`~WuFj5}rM6ukvDl9xqh88W@_yF_fIu7z-gUmpeTF z<4$cxh%AvNU3=O3_HO;);7!=Gp`oL*$U1@ZEMZvqYCv~x(-SWw{Fqi;cPwx_@z^0J`_ml4~;k8r_osyxd$=(7)Bc zib>NQ>6MQY1%1z{GGLP|w$R=D4&3Js9Q{=WKWd8G+?pGHb>4`ap=ummN%N1D^gMHC zHnOr&k(Z~RjfDK$S7M8Kd(;#1E%A-owQc?XwoYU!I!+qPMtn264eMoqo1r*5UtaR9xaZ5r^{ECazx++p5V_hENO&D!XDGQI$PF>wwy95z-`e#q%0dwNmaO~*O z3Br*xW=>C5_ZB^<=ghR2Y)y<3u`LF3jb-V=rhPQu8N1C_XPZ@J8LqHEftL6SC|-V;C=^bZeWx7SXdG!jG7OoEtzD z$2$9j*1{e}CV)H;O9b)X3W!_yg1GqRT!@^mT|qvd6;45Tl~>RKgU|%q>sg>qq}?IQ z1O)|MeSRj+shUqdS&H?Nkv#2myzRf_o~PH-a3EiNqq4b8I9Rg-th0dGb+ zn`SZh#S5y16DM*fdyDwGn}i0Om)aCiErJ2!8xyMoXf$SP9kabjPA_ufTL$=>5_t1S zqt3q!6y*LQ8yFIDiC3yzdU|1D0ZS9xOe$+?zHFPkrl64Am+J4&AeLsHt>iuu;a|cK zN-){KFjCWow*?ih`1NbWi*nN1A2+pyQCtqL!1Xn2U7sj9UQ2^24;9ax?_| zb7lO}YU>~fR5~)oi76=)UvqBf1xH3=Vb*uKrrT0dQr6bIo*o2~{QQWouP?2bBfp=~ zojZXENeKy07qo`C6LD@FDWZ6cXnt)da6@nh5`7K?6$E|p^z;n!-%`IH}h^Jc6) zMNsz{?RVEQ+LUVf%C~=NTP=+QppqPFJ*o^7jvBmrXxW|;q|voGU^n1$oRTvC;f$6R z%5}>g4vt6jAr={b=g}aU+N~6So$S2yZYy2bZgL?Z-1>$0B}j;_ZD$iCOJHP{njhFe zE^E1dSjMpvM2kya$1p~Ya(8UWyxdJSugMqsQyF)^J!LZBPcORMbjF-#D_}XuUy}9o zr#%L%zR5W=Mv`sEmJUZSF`V@IHQ^YdY8DMK(UG33aT9i^d3D=WX!o+4I7 zXt>cpQgRO?bnCTKM@B~K zx{!GHK*OrXN*Rh%R;j^|aOW!J^z#3c;)7Tb}v0 znp497VD(1R(0#Ms!l3@ed`l1himgn4>nOy0pu9|PEL@qV$*TH!7AE;19W}{Jith5m z<%t$IH#bJtwfApcPR`mW9&SS;n*#KkwQ&pH6ll*u&byi9da+G z@}T4L#LyI4K$vt0oyTj@E9|IKFyOigK}tUkst%P_Cg$1h+;Iq9(<0VD9sw4anfqKf zRf|-qTzlX%Zqxnt?gxa8_TY{}e&kz^z()!Cb$o>5bV=~p*58Lq&o6t$nfX>4o^ zM~Mf>?zp82qFj{|EoV46<4dk^1G$9d^$Pu=2m+$+=Lc~J)N%sy7#UQ0N=nMXBd42r zaGK z(Zg{2^K;#~GPLJbP>=5}5&AO7 z;&_B)TM)Z-Wop&1`^gT*U^myFkOur5E4HHgNp2F;HVLpHuIBYR<3VFd$$j*X84PV~ z@_H+5@G7sm{d)Vv*h+}q4WE>Ju`talvm}gDJ(!7xfJ^CT{lH1Q$|HN+!q3P0*%Dki zG;tXj8IV5Pw96OBcK(FIHasFC0%AI6QBe_u^kGg30ryeMFk0XDjwRTk_Hp%)a%A4`>Ccdu)#T)j0tDSERjG&_Tw>TIl@-*)x}fDmO@=3}+nQ3N|8_?BeGKZ$z(m0Y+t zum9uX;UAFr9(MkBadBs#W*qo$tBdRuKF^-@zPWYjd^nab$I_C)CY1;OS|Z7LsAXta zjn)QP=N60FOeji7Ji4VQ0#K6v{Jn<{e?n*&9vJ}*PiDJp7vL-Nk_t0(N@jT;aNhS@ z5gygD+XK-gaz+q2<1hOio(8b6%9&mIx}s|jrti|$qgqcZS@*m%iX#VruyY>ygoAK z0x(5DXtWWpSZ}ZdV|7*Gq*;>t#-&#xwm$OS7Ct3{W?v5-Il|J3zsDitV#ecRXWf(o zKT1nU1%4VEACD2XI>F6t0Xa4?F_D^@8dR?Y{XA0hz*n!XxkEStO_XnK0DSmS^5Xze zFYz#1zke{^*g~}a8naaKTbJ|}jP+}4Yvbe?%Ygq2!eXKedSjB+{5EM%EJS{N*Yz=w zIO^DC_g4>mjc7C!NkQ%3rapEoiGBi9kd@WdWSKbn9myFCM3sp1(RO-8BI`mB41HIi zHV`mwc9T-{j1ul7X5# zk;x0I`R#~_hT#Vf9+Z`n(<*srZfu##?(+l-e)AGAB1&uBxtXS^tl(Hg@eEdI|`b zfPjDj<*HL50gWZlV*-5>g+*rFZ@+V6Sf^%}C)>avghQ@jN2ml}ksH^9jv`&?B+!&oG8@@93Imqxy+Drns+tI)pjoZ;=mww(>1_z6mJ zCOFS)F#&tjY1}7@)=fr=J@AoNHN|40YYm7KrJT8N6BSJ z0a&{Q@~Xmxh5eZjxv;n-@3n5uj`SEcuq!>PDv2Q_U|ng97IT8q6WD8M85!U7)oU<0 z?TiL6gEk*}R?u)Bqbe#;Vr*8qABuM@2|JbZ9q9wGm-p;HDOMgN7`yqw_rSwKMF9qu zMXtCoDDqC1Sc^oRJ9o~8$sI8VoPOra8Gin}Ka&5XZQdVhVKjJIY6F=8*&F)%`86~) zW~D?UTpDj;uZDBysRJDV9XY>F*w$(VlIRr}7Iw;_^-??`+wKW7;t^=%$JwU|MbA3h z9zS&9SRCL1*s?8WjaB-B%7BJ!64oMSfXWou9on-uab8AZ^mda>z+IaA(rO-N98x!h z_WnHWNL=Y~DZc2jJI4)a=L%_3Q&WNb!xmz+jEpEy_&yTkXOw`h|FA3=(6H)_5j@Fs zox=Of;4GLQ`Uu@ZdN7lK{}T2h8W>>-Vu|iZr#$yOaEE!<9~L)tVEgWX{>GWP5se9r z@Re!(6}1>J9ORjG^9l=p9lC$=)G2RdJdz=DDhsqk(_MDW)cHCm_GRCu9(vTbw-RUl z6fKly1M@FsY2LrSy}m+=@mk~VA#_b}F(|T{^m|yZ|0^;4=DR9o9G2!R1s$`33|%#*oX^?d|LOP+ zoXQrSOI?jNT}N|e!wqV1S4~rnx`wBS{|{!W-7TbZ zjMUWoU-B)4*8d<9cOlmQ7z+IVhDS_g;^nmlR|PQDDmzYxJ<#wchzW>HK;8sG-Lxu1 z>JW>*;!MrlPleI~8aW2lprk#E4>JP*4P5%C7LGU-Q)tPHi2>-JutJ04CtaW_F#nds z2=oXAsXwidJSv4ZA35Je@K=nwrq&lk+Yl`lsV0tD8vzkA2><)|IIU0`J^%`#vTSOZ zbScrq-rioQ?+6G9fsddTdb{uX*Zg~3O7WK`31uIGBXJ0=w{K4`+}qsF>9hRZCl(hM zC(Mn(M3WaC7S@rikMjE1lBSvt+@?1v*7pOXT5N1Ah^F8~;kWG9dDiwBw9(I>KL>|| zXy+MAH~~+YoRs8Gcb=DtsdCchokib9HhMd~#KdG38mWFxw73fbhUtt^mXN@WYMt#X z>6^JHG9fYqkYs`%n2pndU&~#b0Malx&r38QI^$L@!dWb1VKFYBs;a=)@r+DPh9b9U z6Z8c91AhhJ!`imVb;n+It}Tp!@QB`8vnr;BOaiqz^iw{2_6)p;U_YdJ_Vg(e?(6q3 zL9jg3Tb3(R9Z>60GBUR64O@ED`7OsFFXgY6w0+^nn{gVhIye(b#wdQx^>VV*w-Qj@ znr%NMUJdWJuIE}#7A8I11`vxJ9AZm*^2dS9AHcDJYf7!(WmOl2Lh+VbuVN+6r&fDX z#xH0)kS1!)^7eUUWhIES;4f33s0UdVG{M+y>G@a*_a~&Z?|?%^qmd6EB+p%$QzJlF zNQubEqDAevQxm9oDq4|i$&@8cF)00S(xQpeStZ=e%+TIcHv3b6{)|v$VB-)pGduv| zbHWhpK*q)spl6ym4WW~k3dQNVDDc4c0ah`aRd-%7pLf~AdvWu(<9d%FS04#AWqcRZ zDFqo*p{FxL^HNgl4_?4-Q1F4&cFMf<;mIMsHGPM%f%MbBR@3=|(|F;9e5O;^~x zMmF_H+dvwwZ#bRsfQ^IWH7StY^TWP8{kC1YV_^iRx0?EPv|$g!`1RYjZ-clCv^cC; z?{;i2ef3pHxl{Xp#iYO44?BbtBh11=Lv1FSC7jLP0U)fbyf`F8GdVd4dOS#?!{0&s z(b-mj{Mi8~06bK!j?i7o54!5R-n(`x2w&f>l4?apF1FkZ#yg;#)HAhIvzAbHO9z(8 zYtFs}`xR_@a`Cl`&qSl-7g2Vol(c{oQ8Y3s@`Jl7)^7*`cDqv(!bV-=^6;gu3UNJ;L7Mf?3tqs;mDv zj*cy^NeUW+3QB_>JNealv(98#REOur445C>70^>kr&$ddL*HekqqS9e?vO;%tGZfp|Y`~mV- z8G_jaV+Q0s=vFVsY}_-?h1SrvS$^iIYierG1?9D5b9K?w5nhdcvwtN)T4a4@N^B;1 zZKrPJWF-^S2+#|Or3@vXgBuA%>afP8Zj1g>%`c}%zfPG1B4r>V@pqsxON{3v6?#d9 zdgq}b9(+TNa&Xnd2$am<@=Pwt#2J|*m~DfT!on}e=ouK~E##U;d?-2LHUwj9$LB^3 z5vQUzfPJu?>lcId0-#wJe99)ADp}=qNL-(GzG-3r5l-nY3;s*hNzvR1Qru;O2by1vU|9V=xR!Gr0{ z0w7Gn^W}tqb>I1BnoL`@a$-d zP4+IGZxo$NKvf^88+$G73p#fq7{%CI_`ZR`T!gVWoB*u+td|X&qOU9HU%vbaAhhva zu>a(p%PifTErArje5TEDPaixfBor=xX{iFl8nC~&3D3v}M+66-_>?WzpiJcUr1!vk z<|Zxx-&cDd1bhwd$LNFR zn^O)M4-i=^5jDxdVQ8;d<~hat0_IXBqiqY0OV1j>j4Ba&&kG|!GEoY_(%(j-(Y(w# z)a;&A$$hi}rYAdBFsprb1K^Cx)Gc}52}ZX5SP8s1;XV3@f)~ZX*~K|y`fx{SXdTT$ zk*Xl+?_rCzTv(>-1F&QhQWX@Pk8P-l1B+L5D=6i3WQ1YwUc5TQ0%qatp$Gb;5f6ba z1Wh$og#&9T<9DA&waSPPMZKV6+@-YgT+)+1vEyF^&&eM`b33mjuq#m8Tl5w#0HU8b zo%#`KdXq%hlOWMQ`TC?E)0Lsg(h1C)tiJJGAn`z}j2cRmBFKf_sT}!t656ICa8m}w z^yo$48x5kEl;SN~#KqeM`F>=r4|p6rl1Ki<_%_@_f_2H;EDkSKfXI~Nce%lTTExOp zk;Z1*KGTiF`Q=G}XhuNrh(rDHV=wv`w_cd!_HHpe>&ZtwtuS`oL-d z!kVjRBXUha*4?8J3>FZ;G74pF!+%58Ed2^`Hns`yQ65s@_GVd+vG*~NKUWBp$q+i! z4-oGyKeg15(0yS+v_PiqDu?sBrpK5a1N4DEw6L%MtPiB^l`FMS%vI^azRfKRz5eZM zJ5tLkVmEuOb8Q=Z0I)j~h$&}DFQfF7*!lBV);|L&3fCMwg1WKD;(PHXbZwcOVqv*N zLIk8Y$c}8>oEIDx6AIbxgJyh(1De$?q&C6ffUko4Xq1%NK>r0>fSL}P29JQ`kiPHw zLJi|euBI4i0|G`gDjFJakbeWtOLbx^`@@gWEBy7JKTmK6*Tc;%;tN$r3_Pa%zlZ~= zcK*Br@OI#>pm<$jP))?)u!!2wgo6Yi`q8HVy7Q8J#Gs>oZoZmmj?yTBwK7$Kqg_2~6KwOY zN8QbHtxLsK#Ms&V*-EtU1&36{t-gB%M>L0=y%mb&|b-_dk^+M+8j)l)Rdp~aM znLQYbGcT({IiI%J1y~43Rp2zJ=gzu=Kkka{l*Q*zcB}ysQMOT54qgl2hpTw(7a_97 z;fQ~sI5)#|J(uWjF7{teD-p?vedWhA6ulPJ8xwbW0#&1wak6}R59~Sa0XJf^0azsb zXU`m2^uqV8s(x`5C*yv{>s-n|4pW+c;KPQ2dyh`*6Q+{xRe2rk_=9PGW`e-g?#6K< z0r^nVvRvPVbOh~N1l9mSZS@2 zUovZPwcfsuoeBGn?6yV`;P49g{Q`KEt7~Hja_gtL{&n2GZ3LBe@NaWL=r$)2EaN!N zfqtAemwEax*1aOmEgrB>p7QT~&?J`ow;QjZ6iK)*u!`F0DISd1_*nt4tn5oTwt+a9 ziqG)bZkza_GSg=~CkKiD=#Z7Sl@YN^{9ZJ692xPeA~qNWcjONE);SdahlKcls+oGz zGU8v_v;Qx(jE{UrLsUAV`!pbck=Yxp8{NV;#6ZFkr*-SjodduBNT5vNc(jw=KJf!$ zR-eXOUGF635nyY9fvrc!;YO$ny;}ih*$l7_H$OpGd1i^JBz8j_7@{%ZFJ2Tj8_CMc zCwZMUsCs%H2fPe7D{V2WW-VM~FhDI`6jZG~nwXP&++;0lf)ZP4^(9U|aOZZ6Q?(VedJ+=!2~92X7kL zUw@5*87Es~yuWkV*OJH}x{?;#YLA0FAlxD^yoBwVcc6#AB29sd0o;%xJPcuP9QMYR zQuwQ8mg%>~J8kw`3Tkp(xX6;>UZ?#wiNFUyU2JFtv6Xf}SYZ8AIR`G5ZcsJFmcH}WX8me!U$})()7?6QJhP@xk?I;3W_*W~DQtG`%d{o=pyH}Ij zWJN*iYK;-dSIzJ11HSZ5TijEoF48^-F5RkRwtO7DazV9?A@ z!Cv-;ecKozmffoj=8UdP){aB})Ye$IKDWjjH^Ud=E+T%6A~w)Sx&o#e9&YZ_0@tow zIYiq=0G`5)gu^2``Iomgn@mhhK6&gr6{PBmo%uoZEG#PD-c1XxPeQrj2DOZknV=~s zToihH!Gi^6b1!DJJ<#=nVguPH|s|n`Y^78Jz z33ojG84B+;a6X(tab-t5k0Y^Y0eU64-}E1>sWADUu$pv6=@3|Xs(yXh#79qRx`r1_RPQM6W0!VIaozZ>Vw2|@j%pI)ZcfY>*eQ&1O@~aTg|+k`!28zmCygY5SM-6 z0~c)rSDH>>|2Poa18REk_xdX!SLNmVr~00vN2wPHvwSkNZv5K%O_4k2$3Gd)Odv{; z{!Yhh>{qPCi2k0^-YrWoTrCsDz{=>+-~RMWlhqr9Tv{)V_eekKjU)fC;e-&U$;9vp zN(^*rP7@{`eSL9{W%zxy#GjW8>(<83hN2zpuHD)A-~ZKr-c<3YiNJPeVFP_79))Rt zY0)(5%8dfguTV|Ej-!!jh;1;wJ(ALdB{*NcG@;8s!freMD72GdcOq@I?aDz1-=Uvk zpHtABGuS_Q1FWVCWb|fRbI{5S9TW1&xd26hfPBI*Phvv`&D}i>02zQ0m$yUPPIhH8 zs%pBrmb=~uiyXi3gYVb2V{zGS)$2nY3I5?r1Sz0GA|WO9^Y(Ta@e_KsIypFm244c` z6iIfg|1B5v8z5r96)|5Cu^Y5fET$XP-S#dBX+JHA-e`-NEgbQO*gjiOv7IUS;^j*R zD4cX3Gzehn4KTXa+hSIKezWv~Mjg0du@ic|pg(y4-U{Rj&;tzb-t|^*gKFFnZsZfC zBjc?4J$izq=0k%Kd$JA_BgvIi$eSo<<=F|Cl;#4hhL z!75AJ1T@OxXdM*04G{t*n2qTjWAqX7-oKF`%8H86+$wZoYyM*dQWSl7J7p6{kk``0 zB8v9*!Y_??I?Ud>dPMh9a;h~4gq}`Lb_H4C1l8YtEEC-4cHGe=1*RRapBxODFPbY) zo!dP*?-Aiaa||o?0ur|#si)2c>x*O9noJ_j&g$G(81=yRtQ{m>MwjIl@Ctn9*&HBb zbiT-$plQJOHVc~90&i_Cw<+9cILS8LA>}ha5g*_RGd$iDGdECyz;-hU9G&ZTnoA6W zax+*J2z&3Q{)xoZL3eVs34S(laaX{lAS9gNfpQkqhJ-V)4-za8xRIj?yt{-Q7qWrQ zf;gM(hrQ@RuguY8BrBxZp_Mp>A@%FUfXRdEH==_l@oa=2+>0OJ-mehMQI?b8Cu_ht)VkHQ5?VWC>=~`&NB~!Rg4p)fab*fB zsj0uSBPVE~EAEaDl=k4Sv7w1TAPM*}h(&xdu?uboG3;DcvLO2k)o!8~MAOX+B6t^% zfs)Tq=Gpd5FcURjiFZ=I%^ow&R{gpdrxoIEAc>0r!#i9m*34y z_J{axGrtFciF||>)L#A)x_8X8pV=jgT8VkX(?MISyhd?9LQ0oE;hhPtJR)b;?X zSQBW5F9dnba!&GBHW<_6TlP^ct?`X|NhRc0m%z6booe?5=_8pW`UMZpa`;l zy3R@wgMhqc{EawG_*O*2%Fdx=Bb{o}xw+>z##+&^bO|f>A9TrKKZOW>c1Wku%OJ*C zOF44LekNt%DPJ=Hd2m-@V>=1h1N6H@zV_&CK!5_k7m9HI$ybeaAKxe33c!O)7@j_U z{1_B4h%YYo)Qiu6ijKMgsHi}0g|ndm`xIz+)cvlo=G&>yhJ}?9E z3oO-2Z-JU!d(67MQ&1=aTNwz=yq|2Ylb>*baq+OgxPQ z3g&3E%m{3{lR;0c+GL-C;tU!zH9@-YEh{;L-a&NMND^TpBk%Prmwl}N3sxm{*t!pv z&32|o)p>%m`EqBrzC!mJ9ONotF_lp?99|pa2o(};F!-+xG)9Ty;o*4-Y=~en?iwQ@ zT$OQ%_}CeM?p*=bqA#pN7@q-U{ltkAz*b7nEyJ!~p4H3R#K?ea*Tm~=j#Q8o(x zOO_d7=&Zmazi?5-+v;r@_S%D~cG+G!5t|7>m%`>f{8lQtbRy~Sv|v8yX;A=o-UYav z7On`UI&x$Q?%)k3cUh=D4vuNcXc^jO4=f0V_zgpp*>@85Mw~>jE^pX4Ttxr@_Bgi= z6KNu$_7KLS&pW)ts>{8V=pgrX`CvJW97?&ztkMk^`dU&_wm6=+?zoi)fZV@y=@MAB zucEw&q7c;=QHQ;g?w5n`3PQx_=qR|B@7(!+xO?lcs@AS+98?TYF;FZ-#h?V0Qc?*K zP*N!gQRz~o!;OI)3luh@AT1>+C5>?`x%G{>B3J$T{bE-|P3j-}U?B^Lno1 z24O>a@e4o=Nz{!v zuBS^6HuTOYAJuM6Zu+r&xh2#Y8JKeF;=3xl^>Q^OBsYH?7@z_<^Rc{hMgZDDcJ@Dd z!+^6Pd|Q4<4Ein^BSM}0P(T^MBf16!N9HXS5%tNVJ%q%m=L5KY(XUF-%B|!&kAC3i zD5C>ub$7N(0SGN&-MBF@C`ec3fJBhbcgg@%4g6(no@{-1W3|x8)OYNM_| zpK+Pb;tcDSi|A;s=~t=^a?HYU$Ih{G3G4KBQ)SUnaI<|4e4i|FsWM7+M&vwk{37GE zcp0LXIYNd+x^o4;WPWV{iIgoTarNuhbJL5Rx_{{&jcuTNylzawFMZ?&Rrm`jZce=b3cp!e0N4 z_YIp8A*)Zp-H(oApA%I=Zdku4nqS&)`Kr?A=an2s`7n zYqXfhi7U4=;p+>r3-Vqh+ZUNK352OGY~+rrbzA;crNL;3(DF-N{d3X9qALttWsPb3 zrgiOZ&RWuStd@bc^Jkez`*A*#VFTq-2<@!#wgW0Pkr~5cnJXFAPf$MMrg6UP3E z>YA!kJsl*OG#hTl5Gw73Ik~axb^NNniGqhC5>(zz7Lr6Y)m=(duQN684`d1L(EiB+ zd$&zQJqSOy2T|1xzZDI=GBcf7z((Kq-{~o!E;551T%2&E9&+5-qTr|R9r~f~v!?vw zv}YXi-pj?>{;CcBa!UARwO76Bud42!ZhL=A#{cear#+9-QS!rC@0V@)Uv>Nc`2YIV zd;QBW@b?1h$6EaEiuZdx{tsikIn@i{6`>V>@+skL^jbxk?LU5s>X)N<-Ew|&VsNrt zFnw^=ry7>!x5iFLdd6tXv2#$*d`!Lm5}@ociAJbwmo1rh+=4Fl+4po@Czq=xF3#qU@B@+|xvOmX4} zuwQ|iX*d)X@5g$OhePrXvn0Ye>eh4HJ4%IIbvL*+Sv^Ox0;?L*%{B1!V3xCujm@81E4(YVN9ApC z=bxRjy98r~c1bR!?|jG+mj)_LQu+0+0Us|S4(P{=l+pZz2=s15v4G{VbB9?=zL?XJ zrtO}DPA~yFLBN%8RLb1Ay$?DmO_*FJ%TGOt90X^T`!gg`W{=RuAaIWTB*LY4RBqj!yprl}1%tG}|}$R&Neq_g6* zqGaaE#S5esyjWv!fN8-c_wDXH(VtfydUmdeBRQXw#Cvf0-HomEp4S2w%)82^bLh^g zBa6RqCQc;wILBvqJAbA2Rf~unb5IIATB+vyc%Y-pGdemuI>$JwTchQuk))oUq~v8E z>acN`F*k+*1ntu)n?@@b_UK8LUS@Z{q1i7wf1FY0ZffIJ zsuRZz7N*5uWWmhq4Q(Dw zm82d>)jfWgs2VAc)W_h#yZ3kBn^ni4QL?<=w?A>y*Ypp1XJ~uS)e61PSC{Hp1FMt| zIS$qt^MkzfB7X24Y*Rx+-M2BfZ55^~B3+BF(a*}4xuUCQWE2$=(gLF;Ew-Ql-QZ7J z?5U~;k9#Ti^DdA&^kDNOQbNDA^|wFAn=zNRa{npqu2y?QzefV>qDY$(FQF_gzb?xqUS#^U?Bzv z2)Y=Z9xr?|9QENEvZ|ryvZw)kIx^-EN`ild_rYgQ1lao^+b+AD( zQ4uclIB&mi>K6LYZTCdV4pQc8v~oMVCDqtW@+Dk*!p6nLwPlNY=WjZC-yoI>Mw9GMCYdexL55e~Z%^X+oLRHBV2qXGlixBqb#czWM{1CFi*J za>-8?NUN$}oQ^EH3&8=v1r!RvUg9*8GGYfqKEf*mSvz?(J-R!+~!Nt66kMlVUes>?zsnF;YvLxwgbG`+dXwB?H>@BUL zzJaujua^f-uQq~p#;K6)Mep7nupzvT76#XP??{gjOCh6Bl9*1b3~w!PXm;`} zm8tM#QGQ1fj8N-I(rG#E=wy2po?ie}lqW&h^Q@mAK(O zuQaFz=)3`ozBlkpl5bjJ$q1NxrK|U6HhB~t!8=34)8?hYyL(Nmg!SvE4DYo1?@;60 z>_hC=h(L4@h*BFT(56j|@mhSNe7*(|!uHCX;eiOXOg^C1u35cWDmD%n0T9$&)V#+W zgyoMwk+@gX24Zn}dHSZOxKpJ6$5AnHoT}?(eHBsrCZC!EyHGpI8QvShjqKxPMpUEM zIHPK_g{fUi2?aK8H{*)V=qoK}Wc2p-j%Jq05}nq9t^oVpkM(fTn1>^<1~;n@D9Ft8 zGc2?K1)uPAu=y_@R@`>C?9h)=mcE*uAj%a^619vk+qUcU z{e6}@dL-5FV8^;EWPN$Bpg=cX7Wl1C(&nB>tAzQ3v994^C12lyIF&CNm)K6BKaCK| zZeuFsSc4O`P9z8;QLyEDxi}Ja@|@ElHUKPFzpN&7S2TQH3O(5bigWJR5%2X!)qcm3 zO4UP@AtTqEI}h8IkC$m*3o*@e{FanzOj^HsDCGYA5LBl{W6hIs_KYe(nsFKsq{N)( zUkGoOIQ8Ax_hA8wI(gK89@m1GJgu}gYo%$=aX92Uo5pSd9-ps!q}0Z9szarB8T?FJ zI$JCo#NS&j{JMeoK1ZdjA;Gt#k2hw1_v+Gz<41girVg%~`3znMJ%*oQRIBt&$J({r zhFWR5g={mIezHGz{UB@RHsdTQ%Fu%MlO$>wz@~2$R!5No4B<4H{vt^pQ;QPc&g`Yg z@uNK%mZfFX$mlOaq_1xr-MF>?!?rX09%TQ1jP%WZ%M|T@z8&M;L*gaP%b6wzsE5`+ zaM;@OR%->_v&hD8-I0-(r#ec$+i#tTmFe=8(JLv>q~)^jtoctX?mEf8*j)p!^uIsv zZ_`J){)?~E?Ac;`h>4~!h--Eo6^o~25gKKMy7aXNY7#c)Jp)h_Z8yQ(qxoinHL)t@ zTbA1W7kZis$v3Hsi;Pb~H6=Fyg%semg>DQrN=^%k#Lk$0~CL!~pL=vViQ>mlg3 zn!ZVGvS^jyeeIAaO+C^KFyjtaaXZ5OTL%-y(Mk?nlZI#4O0;b z;nGcx8~UB$%+3n2FPO+Z*u z-sY(0>KDbc;nLlU+e$@0J+P*GFcZyamTsXEH@xLS7N3%1`^iH_GcPmE{I?`x8Q<#k z7q1Q;SHJUM_9dLT#0$LCit54cxFoZac}oUEzGjj|csXp_mz243eSP&aO1^@j>_@?| zL4D+*&_+a9j_kB*Er1F6y=jBH!^5^C$((*rHu!tZcvhT81WOCt{j4;`a<>MkY|F$R z!XVX5h^%GGtCvT5SJVWP)mM~Lh0p+|H^{$Jpbv6-!Pi3Xis*wCI!L)d0|XCq5K#o_IVAUK#f@gSE7I*dcV6540;tM=_`NElQy^&#pGi3kmAD zk|FC$`v700#frN@?1JxV*RBxP@Lv7=#!?b@Tk_aE!plROP~MwNW9ZM>f$pYUrwb7* zV(j(KfaB360K8opMSy#7m_>R&G4-ZjVUa6}|E~4lDU$*#Q zdH?Xh=T|oYaKFsZ%XK`D#KiWjy)W|rV+6CKTui`?aN*XR* zQ{r{mPqb6o{4_d!Vs`NH4S6u@g))2OMFE7Zh4VmOO5s3#XD(jiI^idC1M3n0~_*D#zz(E$(O@!B&X;?yVipfde{4l1aYJUsm^REki##Axjq zoMx72TqPwDwbbzLUN0!*9U9Neg1PB8|1qP&u56n&`ldMaIx&R>;$akTtRp$GjNua2 z09MVbW6py@rSbUqbxF>2*a*LEX$kStLZUocjG-B+=m+%Phl=17Y=Tbg8s`;uLDP|_HuU<2l=LkgeQ~g`$hat&T%K=4mVv-yc1lEJz0KJ@;1>?_&_c_ zvKtNuVY0G!E3Qo~(z{~KEJ;~?>1mq;5!s&l?LZrXO5yc55a2L0G(`R8O%eqa)iwtC za*mm?!=pVgQ1}LM$j~ch{YRsSK4#u}n`>K0m{k)vO4~ygq>7EV`c^yk)eC5s6&F9U zqhH@Uy?aM_W+4d`V$=vsh@4lcCuqx*F$w^U^baP4HS}lwdG%}3%2yxRO@4?SX53~% z-zH6v>53fDQP7O|WL0=*xMLbUa`sTTthBeb;xt|gDF^`Mv@%3|Q!2J-s_`{N>59Gx zZ}ISt8`rJNM2b^#z0)c5tWw3j=}Vs>xTauJlbySC(oS5UA4z>A>gk*qj4N-bERvOt z8X=lW&Z?@bWK^Bs$L!d>yAM{i;YFca9Fda`7LxQ-J7?n5$D6;_EGD$jhnkX_>UNyg z>&2p`P2#SZarX9aYHKxXC|Dq6_0HxzksK{Ag7PZ-1w6qhx3t6ZfmCqC&J|G<3|MvT z$6rnn2F~`WkcwcZM+H_N=~=dk{f&ustshJ;^bU{%CT?MGuqJ@A+y}JwS`NpeiV*)& zVs?-6BXh((aPt5aCmR;6t@o_l)u$A8Ta1rjk)o49=it1mQ^mYFE~o8}R|C02%kxs8 zP%tRlIPKD1&5^yZgro326@}QTRMOKcHDJuj_F&grR6be~scIHW;0cSmFSYqmbFa>X z!hKRWEC*T1H&a?AMh_RELf85Ng#L&Sb*e*x*dG|{;b-#@fWHlOvs~!i=~N=(g>q^i5>JsXCq30=PFl!A}69$U{C3 z;kWD2A^qdWncB9}-|StybDVeQP9sEtGSQ)>2}J7@9G)crMUEbI#ZyJYRsyZlnfonN zFBD438?8r`TWkH^x-rurb7M}dL=k>Kj<*iC$+1Dz_0hReZ_?Bi$!=f+Fz1AJ&d<0V z&q3TCugmce$>-uv{9$R?(;IsxWPf#tq}z$Y3*X=Oy*rU7H#D4GNFuApdRKU_FY@Ol zA&1AjJj=Hv8v7r|o65Z@SuW^~N1hZs6#`{FAbJ2D=l1XkMA}pQsX+ zxzpg?CoVMeeAy82VH9~DCIX0LU0d(BN))a~xQLd1)y%>Fjsz(&BUp|>=#Q4w6AjMW zWY3tp!CbTVTtjpMhUCV~YLmlk&P`j%-)!e@AyKE@Y3Fx7SCxz`f8E>=;d%5;ym~@C zp!&`Vj0A|_@v^_A6iB+>{77L!8mdYAlQ(6tP%Iu_{*JLO z$w>^upzesp7kW@w_6tqHe*DSPh5}N7ee|k<_G<_g`LIeKK3L28m1}IZU$^@XUI*Ah zR@ei`suPV64 zU7Cf2MxW)n>c@NZG25wm4Xi|oaIoEG<{~c_&*Z~23-_ELY7Zi@WX2N0cM1?zBhO{LD8!6aJQO4h6?v{=EzlC?>vz2=z19O}p;d@vT2HTi4ov zzcSlXkBJ(AxGr&vlBfTZ>k3Jq*@^99YIMIR{$}@yOMk}Pf?@zw-uyl8u>`x8)+zU;v->>JDy5Dv=V^*Z5$X(5{mlMTg`S4+OcK<#A|#S>wv}_LfW5 zmw9+OX`<-+>7KX_v5xoXQy*J$UVMWZOf9P*WIPf}stziT)~AX+Uk>^Q0)y}aWuXLcT5ys#8< z&JDl=SgjklWdJ-XR%dz$%@A-qBG=HSB1MxL?*gYTbV^#u8q-q+g5gWJb?c{lV)Io# zMvzQ{RGAt&a4K7{(qDMm`@9GV=c?@@`}=gjGK7lYRt=P_cDThSZo`W{pp!5@nCs3Gcy!q|y3 z8}ylr>16RyCvC&1h1-%*7*q4j_xtPu;J&``BbhCd@8j{}fU z+v01JCZ}}{A3h8a?qsx%%!6B{lb#L)6BGO9jpX*AG^8x?O>16GMQ^&DXaGBBi=e?4 z?9Jgf{JEtBA>thgHQ8H|;g8T#sEO5Ym7tk_pR-VP1?J##6$+)-atzcoA>G**-G}wuk?)^4m}C@-}r00!zmdvvkB%wWEC^TP50T z`|th?v#GP&vP^rXf}f43oC=2;p$;}FDe-T;UUfB4X<&XwF~jmWoGUd=FsJ%Q*V2F z1KaPqk2%apmkhP+jyThuY4hu6c2}%rOtWnz(n*fUJMTw3y#^Un2wI z;{p%MN0uIgJAa)eZF>Ej3x2Hk(=C8l1t1ZcR;>-JShF*BDXX|nj^_v8U-v$^+hGme z>><;pxndG9IUXhIHd7&Yx?fQ)ep8Vy-Q}6h^e?d#=WSWq<%V42#~3_}`q?vzgO>lT&Ejv<_ML-!1m<$J>9r z_W!V&|KTamoa6?F;(;8Z)JRJ(T+7NzLN#b|2rHe5UQ!8~R z4^JuNP2AkQAfz5R|M{vtquMpPitwqYkey-r%i#vIZqy;b50^1ACZTb6^yn?4w=N5H zr9i8-sk$xu+;*TjNVowuL)EttV4_|F3yIyR@b};M*9|xJNP?LJ>oUhr9?a|~FmlHp zfCnn35B+BglecJ-sq#H&)&Y5m)aU@dHHG3HmKG$C!TJ_MVB2O?1`}8&~K_mWCz~HDa4@% z7LlhqFd~9R&&=$AX`zi!R>rTWioq;SBlye;T)TD+3gXnKz2gJGDin_#Sk6KwJRw_R6@v3=t z$xG_P;XuN+81Mkx(5jR4LwJ1p2)!K!5!_9P%Q~<3(*C8dS@7+&H2&Yz~*;`n*V4r&)~7i>lYk(j_bt&4m3F^j~-hw12V zl*b|%vjAdksUX+ zbGDnVg$2uNesFL7lQ`Zm$dO$aouKWin*I$Y-~I^IjNuPSYcE~BFC8Y z7r zr9t|(-P)+bAMP=D0Z(mh9i5|LL!j^4Iyjp)lc;WnB#(_90u~=a$Qwq=76vI!Q$hyW z;CaEL!1ls>!V;>#gO8iMwmW-xkOiV=C@<1{ z{QP7@stUm}adUI)IuSJj0Uk^yRCm2ADvBsl9@K};^JbUcy=KulYDb!&Y%r%v&Do?Y z216F#p@!->mL30O&rG0+9$DbdDsQUGJkOTy3Xf8p(@@cdjbJ55`kMm#YJ@6-J%WrS&k8CUYnF#*`e7^?A zhTqcdgiAqE?w~v<{Ln{A3ULVIlzylV&%*-yKJ5&~V^pX{PWK=Y3I8t11ut^Vx2bX`#wDkeq!(HC)UAa?>Up=t!BZcj1EN@rmaiOCfxk?CveMd^`mNe82NT^4P<5cFGsld}M!K&f>?IN0=Tki4bU6iPbd+3CFKn=&JBq z1f(-FF)diQFy(AS4PJbNLbDA;HhOk=&Kw$DA-9nUcMpYZ^nX6cns5uy_GS${T4Ls% zf**A$uiT+anFzjwyf;U#jR#)D=)$hvr3epj#^uYK;ezXOo@2i%8qs@MdL*ytW6WAN8tdpwLrqa42x_O78~IpW z-5#n+ut{_7fymcHARs{cR~{!5SKrE>z9&{*&ccyIU~dFv`+&`(i$c>)kOV1cA8;OK zH$MxF36`r!T%Ay8r!dkH)dBgq-oKOOh@IKk*f6kR3|^Vds%-@)5Bp0WO2srzL6yu+qK;o-eyhpgCn7(JwE;Up z7Yz}-kXq&XSvEWDObbcFI8hCltadS3nfqq0T>kTuov$4a(Doh->o5NzR{Sy&;< zxp1};zc#gNUaJH<^BT9ujwk?-&Tm+^t`o{EaDgFk=M1b}cfh9gkIK{csV3FRi$niF z6o(LOAIPo5s_djrLwi6N<)aUAs1IZT@&rTjPKW55kK#_`&npfuBZ$HXxCgkoZzYCd zhXR;2a5w-v9<(81?q2>F8k3^ z5#fx)QqqJ%Hdt%`@q+Tuo(Cfe9NuH9W<>%nIu6T$s6$Q0xi_iHhvxvwKaGHTPgflUy;G1HI`3IiHa;s#% z7WtQ9z_RPrS%~aU{$xKI5QH74rTMF~b6}fX*T2JFjfh*B$!C7_Q78FlzSo!Mk(c{`@f!a# z-&5ad&-9%i467>bn*;CsN^3owm{qrZ45S@zTlWjbYtO4b^DEfQ;&?xE*KeM>ozwBB z*HV&CeabYM+`%|oD~x!0l&a3QmEZitXF$9dd{7`Xe`)@5)wMtR=l}d<+I2B2)I25_ zSO-{bTq#Kpw}t*2==5<~&4bdAKViMV|6dBJ`FlRLsd4Pt+)QHx9B8GnL?xuBCYU~y(iI%(dAqv>gzlk^@RnqRa1Q^{e<&}r8Wd9*CKr72ei zpGDEr#GKRFf%dxfY`=b{(Sk0%7zX)_bVpha{sAhYQ0TmHX2nTuZEp=;0qH9mBEfxk zpigbdS~Zh!ZCUp_gg2K{L;z)5X#V5ZX`ZgWIune?@62gb(*nk^=|6=&)+Yt`v-%9V=7s2iv!Qf!leD>Wl z81qlc$nwy%AB>I32kLVf9hdj~g`A_{n9By)vh)YsI`cmM-}#z4!83#*e8w8?XMZs# z=In*iNB;1BeS(xfKR+LVHq9!BFHrJiD2M(GAklxugj z-mrg`xX^yfhl(3O-wbD`+~k`-|FmezK5-!%z;b05#)#^&IH5&}bBbtysTJSfdqtqK z4U>3OMQ_}`4W=3=GQ+{@Cs8;SZkJha0qeH|F#*QH53M| z5<#7!bhi`Clt(&9j9-d@vXFKWRy;ua;KQ70_NcZ3D+{*_oh+L+KyvaF7u2LkVcs-1 zM@%CNi5F*)c?dCSfTD(`W@q)Nwu%vU)Bq*3({)TNDybrHl zU4XBYNtS7zJ-c1(jD@ESX=|;4z_@p?eRef?$B~hecV>DZvoB?CLZeD==)o2Evt|w9 z>)2bnH*oq`JWNNp3LdZk4nS>3aCK|ygJm9kj|6`+d8zlqbs|CK@%IBHpm=EVVXDufH25TW$Cnq!_jLZ$%Da4wwAK; z!Misvvjts7xL_zWdkV8oPha>#ogi(qCEY>PGu*7rI|P75ptYZy*AwHN~j5@L1r z@lo**8{tA#yas2i&u0k_UZ!I(p+Y-YPCGq44SU!Oq~7R>x9N13QN=K99$choKcZxU zrIbL*lx)`>FDICm3ZH@I>pMxQBvo}$Yi~3qE|7>(dEOGtdk>EtjNx1+T_$_fl z7rI+J?MRdAs-3!T=l_g)LH55zJ>yD+WoKm7)z!hsQ8>_kThOI9G&G{InnY#$RS~4c zDry`*Uj4ry8>E5hd5Kd}hbU^cNFAHgb4?vsI>}BJ%F$b@s z`<~C9F3HVXek{NEvUz`DuScC@4SJL7Bn$*$DLyKffgE-?WJ6ry{rD3iOse37D!gR>a$E>K{MSd+~$L&k@C1fpdy zdx>^UBoHZroDp<0oKYmyhiY+nhOPeU>tN?%XoH~}2pdJmUaj#sj}qw~w3H{3^mM7? z63eDpbOr8hr>fXST_xrOc5L2prqV<7;14W!4%HATpT?6fcrjtV3Zwvx3u2=bQ1~p4 zzgaFXW6K8~xMU?-Qt*{yw?p_M=5+yoOYTyXU1AC@_cd7_cJ?DMY?vgRd1Ir40r&CI zaR*LEI5JJvj**zTHA72D49q-_HbpHLlBcBB6+GCBN^>UVpV>&Cy9aAO_`ZDk5~iA6 zrW(Y(Kp0S7uG}{ExNTCBoqyaK03WI=bxU3jUxH0L!}{NN6{h@)Tfq#Lqnur+~|KWb;H8Tb3cKW zSU*4={8ga*{aJa?9~z$|bqh`L|Md(1&T*BLxkRPDWB{lC=Kt%t*zft>-Pu(nwdHM!4GPayPup!C3GX|CJ<>I(Z(#6= zuk2{%xv;P8CDsd0!r;3n-CW43C13Y_+;h&$cH7-L?h;Iek2cW*k*8Rm3wt|PIBY*q z6l6PYO_|ad9K12iB6-Urd!+b5s^21pnw3dxro!GtPJG&5guLH-WztX#Gdbtvs`dh{ z)b{S2%SVcO>n_ZEX9`=a$jo=XAilF~MGEnqxTY42iXWan_{d{(=-{d6aChBqH+v1= zZ9G?Xte3RkJaQRH^sf5LpF8od?M%|Ni}m}JKZ{DeEsfAqh7Lmdr1B;)skdHNk%WIL zVv&DnytvM(nxPOv9215c5%||#Wzaudv!t#ME!`^O?R*HQ=xWPD=ZEpHRrKLBS9u-S z&j)Wk+o8hC4%tiiZgwyHOI8vb<*#kaca-v7?G^2&1NUDkjqX(i{&j@?k9jNd8^iu| zx^^c>HZK$$i|ZMo`<<`dVOfH;m~)4%1@u`Mi6KFq;+hx@#3z5gC} zhIlm}uU3FJYt}SHmtBE>-9!`R+$WLLLNHyF>uof`TsSnNw+DHyJe5c$@}C3D-BpaD zcaZNmWo(kTsDe*auo;xnnc&xalgn0N5fJF&kzIaocMhUaRlL^2i%%tVzL&-mNjEvS zaY?ztbK&+N+2kx$;@IfWiBZCIVd<(PdB#KSw+voxW>}sN^Nt7Ar&HbbnpN`+ufBkw zyLz?A=Kbe%>V^`Y3M$JjTfW@OEAfD7<C?7METRFg;YT)gf3UG=4<&mK8qGSEDo zs5gB-y(RP8SVcrt=c{E)<0Y%UdN6nho#5+1O#w?1Ab*tjzAJy?dXq9TvTN_k3q#);zQI;#1!a-#%^MFY4HSRDAl8M#jqr_t@gVtHBfs;KFa9;!>#l^|K7+!O-ipfB=N&?@`QhJW3KvVQFGSf8Z%(@W1+ zHJ1t7_lEbXDSs6rQQhr&D*f)Ymz+#y6*nmZrJJj-ufB=z%xnGH>})0GwHMc}SdnS| z!NA^Ez0){I!Qblsrcd?hN?p!1NiDDI8R*5sLJAZrqp=HE-j$Vs*c<~o!WSesS8qWO+4fdzt*A9Ila^6r&V_Wa7O7>4$?=`qiB8B1A&LDZpGC`GQWs!V8@FE61! z)^%l(QsqkMZePA+GYMlo-D#v=W9rLR{>nsAJ~D&YEN7bUI*;{366-QNzca&f5~O$g zGBHw0U4Eu1uR-;j{@hd*)LC882BTlNdsf9K#xi#^rtw*7ry6mYe>*$#N`|(!DZ1;? z*X$h-yF}iUO_-HwR{3cCWO~4^*_iq7TbBGtsjBSFrAsHO3l^BxaQB0wSTzvaUp3bB;Y{)U z>lS&TAFXL=FBRQOI5B#Bt%hu9&hbPYr>ZCit;yRPNr2XQifWFDouI zj|s15>C1ccVj6{nnCPK`+ywvbFRzb9h1hm^u9`T6ilOWIbBE-R>$)RHv=85M{M?^& zQpLki!e4a4#LU*V0gwb0GE&Ef)NF)k`La^{Ep|-Gw0ML^n5vNktbI zt6g(>?6Q5NsV1SJf>COEVp*msRnbMez8Z0Ia&l31qOR=t`8TbLSzXWhUR(hVYxMIy zc&RqKo;!s_AY$wI{&J;|;njVgUK-a%;bB*B>7^QBdi(^BUatHDEd~Y&m)5(x*x2IK zc3t?fSRAtre5&BVP!RkqS$=0_IrI85l#-&++s6w@W*^RP|vH4s12 z3`O#P3JI#V7JQ%y9Zs7MLP}rUVwxSHEJs$|*+!)WZ0ZgIB>psC#JeJfU@W5J5v3U=T)Ut>3&^+^~?0K9H8k zW7>~kjMF>~`$|l#h3uej>3kYJLf(^yuu>gc1w-H@mIj(oeCkK(3x{oO`CqUed}Rmr z!B$99H|BztjMcltI!b*OeRx3hlba+>A%4dDOTA#mTe-$(0y zqaB)Wm(DQHG`3ANo3IuSA+xz1g_a4~__XK3GiK}w!{Wsz zm5<)RrnCB_!e==BtP`=1&q%jubkv~ zL3B4<_nn@B0qAB#c=%(SWH20tt~lA=PH|tIF+g2By(JhUv;Ta!I2Dsz4`w|eldIvJ zAK0%a$owYy>ziir?RfYFaZMJZ)SZ{=F)j#}NU)8%aN)u+`@@v^ywP*+;)+iVh;V#&eXOZaL5~;vE zGD%9b-ze(1Cmo83y?lHZK=5f7VWVF1ukzOKTfB&7(Nad5UDH#CU>Nzx>MU01^T0rX z*?Z2&9ni;PG@RfG#eFT&P(8@!#7$`^D<{LC1GCviC-qD6j(R1P;^WWs1yBb{-@KVP|CU~J!kd|_%<{cV}N++5MfrgmrJ!2VvE=ED*fXI_gvqBIYO zhhzcW;dub_i?VuJSFjz#-55$6N1q_%9)X;@{LZo-4xv_uP}NNO4Q-|X2Z7#E@A?$j z`S`>{;>o9qrcbk)>!L(N+@Ds=S>N;M9{`6$t`88|0$Y+8qy(`bUPIF!Y)!yQm=lkQ zDpi%0m#Nfs5Es{M=Quy56Ly25`A6D=zEPWXFt(6RLj++XqksF%NIOij;lHqV`-6C$!Ye%e^ zU{mgi!Kse!?*v?q{k3%IS^Z7+=_g_7fpuitw#|f4k`bJwv&PePw7EC__MRW>aqN|* znKyS^h2@*Fcn2*$cxvvps1{22@?%1=p8Lk7uX$-Qi3oCb56ST?jstd)E8iGsc!8YuC?hHiaS~%n> zyS7!rJ%w*w0nNvG#GCzm>eF4B3(eT1_@8=H3Pdj0+ZiBUkS4`k5hc;ac;Mq>^Zl!V zp^~9FX2Zj>9{w8<7V}mFKL1Wf!%S?OnJbDc`4Lj~zsg7aLQ^FH5kthGzuE#lfB`7e zIM{e}GRvK|(41d?OP}T(5jNkQqI`X;T0a#%-f~GzRTT%%*-Qbr&4Hm`c#P)K-@fnl z6viVS5`KZVY2+kElS{8G&*E~=oVyGhXDB%rr=3BlZ+7n4JyHXMIcWc?^1e>$-?=*X zlG}W!*pII=fWjUCyy*oifCK0-jRe!o`^OStWeDYB_dO64LE}rYbECi?Scv5lx&{{J zfW$G%>tSG^%a_;7MyzN~|KsPYtE)j-h&%b(izV>Roo6-ZQ%E*XkF~I}vRY@ZIkWZu0htf@8&-15~p{F&?tdctz4-B9z4K)_D^L-N{*v0-$B`dTOIYT?B;hRB`2(9 zDDw}@UF~qRYL&I;UKWxGa?sp0#4&7WyyqhAGdd!UUKxMMhJ55;-LKEW!ANtEE`0s( z2k1-VVA-{~)J4ih0q0wddSo_K{*7{JR3f08%f!SkliRtURFadBk z|8eqftmf9XlHh$60S!1|r3P|o0?6=?;AMe;2}aBDSk)asdAYc5_NjwC77(xjHknLJ zFpnnt`x~cv7Em{r_Jf9KEDOKnRS8vAX+wme%<463*H*3s+TAIQ#Iz;Vn9-aBWoxH< z^*R<7F(g(Hr*3!+{!(H^lW7Qibs!-fZCL>aA{hU1h>D7C-`)x~K_(t`L){6A3%va{ z@|&R5r3?YJ@CNe*=dS#7?PUC^>=FrZTiUhD7}E{N4bV0czIBMy;Nqi~>d|*NP;(RJ zFq}n;)6ZVJBA3d%VMF%WcRo-lRPF^+5w`ko1eg7OIEUtd)U%!k^W>pf|9r~c-X5_Y z71+|D7BDTl`oWDVE-oep5t={B;Rd10ONtqpKVO?Xz5~M<*vDa3+lE{b>a#=}0~)LH zf9{3OK~@>A48=n?GBA9nr?(vhN+cvEJ0v7Re5#-t))Gl5w<3BI>)Q(O0%>N|;h=&$ zR|O7Qjj8ErNLL~)!pR2=UMvvaXZR1SGDJ?wRGkY*+~X4qVuI62M4k0wPFnJD6T9Oia>T1uaHo zDmmgV*kaz6hO%%FzLj!a(io>Ls@)hKPM<@v$MWdUe4~-%_KWeS#@->8os}o8bZe779*Y*d-ls(jy*@F;4Rj_;; zmtbco**}QW<=nE_MAzg>fQ&-dPzx3l-c4(H_2L^9D&Vp;;xDSxRseJcV?#!G&moO~ z?c`Beq*Q~*uC=~k`$F>&RYn3UOqE2wqm}P^fMp2vL$47Ee7Ql^R8UiaE$QYtpQu`{ zEcws1zE{~H5jZ``3e6bd`wDY%hyKRcMni~W&{M}<^oogUjCncE?Ieh-4co;`awINlir zP>CTcNVUNSQyo+|dK7{eR`UYaaU0wc7Ox#*2D1|Mt?OtO_mu zm*7am@u>B(;`lq!V779xR8BP^k_x`Lcm0oWe#K~oH@72|F*JT|LDYFUK!!LBwC1KW z<#e-AKBaO_f*8ht5ZySak5HrU(kEcab&zguNh8IK;QO#cH{Hx{I`^qowk?NccS}J5 zEEO1db3f)v=Mer-qlDi!`5w5fK&jbjR-%GG<=}eeR9u-w-r@F_oC4U-}v)W*AZzJr5ZBCs!Wnsa+L!)gAjp5vi zYi5y`4q`g@`5%)~BRvLNaN(%&4}icEdAE|VU5D8k2)(bwZVnWO}48WugNl>K%K?I4gxN-gZde|HwS|isxwwo9| z9w)D)6#b=~&hzTkc^wt)1--q!Xi>!L zWT`g21=pG1tl9)XN!mWtqcv~{11n-C@yDGSvfANT&O zp{lA1ECUH2;i#w9RscTGqJ;~)NmLO~1(0K#Y_QCpijHqUYY(}6;Z8I7BV?t>xh2KN zb35;kRt!?ro@jV7^(O!Ayi;`3-`{(kqd7;Urbv&nksjGJIH5bUmW9Qwt8#~3#4$fA zOl#R&Rkk3uoec#47ISeAeN&m9_QrFEKgvqB!|c0n_;k#i#dXy%BSKcn8CX3QexV{g zmQ=QJ<3{#|IZ~pA1pNEMt5prxU0KhUc}Ti^Nu?zFNVg(G$^*I z!c@W%k()an?>AFMr%CTX0RY-bpdc2D(($V?`v>GFXoN-qDG?_UXyHo-WH9hg>O3FF z*g`@=nC;d7Hc{fQ!qR{D$tcE58pE(TRppN`1nB88!cf@ZhQ9FuUedS%mYZBQ6pK=o z;(3PMLWPY^Aln6QA#H+&aoX>OT3|VcH);bC1C>E&Esxt*br25*v)b|rYk?_tXCPhP z>9D?-Jp#7v*^>-o((f);mGfO~=O9f)V7)my$=VhPPfS#@;MbIsh9Zu$w}ObH)Ik67 z*gpgD%@YJkoKX_)(s(>7Iy&)9>Zd1!#%XeI|8uRM?4jO;Mlvcgl10=}IVbE-pbwM( z62;Aj^y{`#FOAKud1-0Z{9*Jv2Id_pKE7F#zk`lOXfCW&ZY;nxQ<2X7fUq(%b#CH7 z`de%Yc^|KNhg$rpVb>-^WtDMjquhscFWyZ(^|VTe172BiVPF#JbyO-Nf)KNw<8!C$ z_8=qh_C-*r5?-tl;+VeD+1cst?hc^Md)?m8FFY6?yG(^3Q!y`V{X$H-$q?Y>9f0mN zNh2w^pn!yF&Ukp)@2o^7z1oLnQwL(pn z@e`7p?jmCF9m?PIoO=t!8VX5Xs%sO2&wBAQ-VPqeyYM(8$6Ie+q=VM;za2>8aavDe zhy!WO2z6@mEt|LW;lrF%sx=NsOl@q-m7WZ7NJK9JWrh#&2BImp8U!b7ZW8t4D3w|h zt0qgOCR7A__JT)rZ8=L}4PmblNKpO4uFl)Wa&P${yj8p*BSl;@@yz}W*rQ~Z!hr5iIOiQUr zj9cmW1>|eg>gaEb9D>MX!GZr@hOn38&*N*un^Npbb|7>0Z`-&J>MQ3B0OYWuhjv8{%Y8U?A&9y-NMrhzU-#p6 zL%ZtU@2H@KDSX#RFL`6o>3PEn=&~C+>(;B zG6g4=cuF>-0!G{HJ2X+Mj1KQ({szetD}@3Q5=>Nr)dgvWgpH&wS)sPU1@Cc5Jbp1dlCmj zTr?u12G%4dT4FykZ4(F);kP*cFF|r*${()uz`BryKHdmVSU-#ozzonDBhtX_boOxq zcDpuC45F{TIMRLM7;&%M(*3nhq|$G$);)}5*R(#$4aUeI^CdLBG>I}E_pY2@(qj8& zk0*$C*+eJ_beo7S{(!OZ2$X8#Y6Ul6Z~NmNHUx)SRE(ktU|d!)&q8aB$%X z7)z8t*21NuyU|h}Rol{~y%iB}P~`YcFWb0AJbO;RWLT@@25?I(D_FqO)`jFLAxxu% z{sGcNq{v|A7DpA5m{LtHS5qktIORc9FX!hRE}%j@_c$&|w%o9M0jj)Djg5^!Bus=F zx{N1?LjR?xZRd)>LCii;jAL4}rlNxVp%@gqy~hfTgPmTVpj5SJeYPUqDIO{`BIPkhUA|ePR$H9V!d&lNN`9?$W_{;zkA=NvS?^zx7*{V2%QxWT*f~FZ}-A`j`LG;qId;zaUCPXL{ zVghKh>rjbHf7UL3{$ax6glmTieJ0-WwSCDKIuzHkpkQqK^8NDSwhVybZj=jl`X%CL z@vfw8%sF+~t&Y60yCy*dYELj`*ja(WVeTYSCBlDX*Tcc}Vti?Qk?)K{-0txxy(6I{ zi`cI)86x6Ps{T2ya*vabt}a$*UtMeWAl$jSxY5&sDZ1^lUfJa(QfjOf`x8U&e0TBcxNaj zf*xnuiWV>pyha1%`ki^p4BMbzeB#D#250$8!N$T(aBr!AquCDZ)v(2i-d=8FSywe8 zJG$lU?c{dK6%prUQCS(QxT_;!A#w-pmQ>!2rkevMl8Ga;l53qa#gZrS_Qh*q=Aj|9 zoo}uoJftegZ4i7s-ue3Xe&tMJS>jZ*@>n8FHeqw^5odjoX0g=Vx}3#OO5!ky`#|;9q*1;E6g3Tkkpjv@ z+)T@MkoB1*py=Ue^BFTItS$e~7!RBVo`J5erPWo}b-6E>Apkh0v*RrA~}qIUhMig1BM5w ze)DX00uN98byC;PPVW65#wjeoA@YsDVO`+jGT@{_{BLpKr26SAz|J@D2peGVA5H(q p;KhM!1p;uD0=bJ9NY?uE{|Vpc8EdbKdV>~6c)I$ztaD0e0szrvJ*NNw literal 0 HcmV?d00001 diff --git a/doc/design/establish_connection/sequence/both_peers_receive_register_peer_one_peer_loses_synchonize.puml b/doc/design/establish_connection/sequence/both_peers_receive_register_peer_one_peer_loses_synchonize.puml new file mode 100644 index 00000000..114e64de --- /dev/null +++ b/doc/design/establish_connection/sequence/both_peers_receive_register_peer_one_peer_loses_synchonize.puml @@ -0,0 +1,28 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include base.puml + +par + [-[$method_call]> Peer1Frontend: register_peer() + Peer1Frontend -[$zmq_inproc]>> Peer1Backend: RegisterPeerMessage + Peer1Backend -[$zmq_tcp_rd_no_con]>> Peer2Backend: SynchronizeConnectionMessage +else + Peer2Frontend <[$method_call]-] : register_peer() + Peer2Backend <<[$zmq_inproc]- Peer2Frontend: RegisterPeerMessage + Peer1Backend x[$zmq_tcp_rd_no_con]- Peer2Backend : SynchronizeConnectionMessage +end + +par + Peer1Backend <<[$zmq_tcp_rd_no_con]- Peer2Backend: AcknowledgeConnectionMessage + Peer1Backend -[$zmq_inproc]>> Peer1Frontend: PeerIsReadyMessage(Peer2) +else + loop until peer_is_ready_wait_time + ...synchronize_timeout... + Peer1Backend x[$zmq_tcp_rd_no_con]- Peer2Backend : SynchronizeConnectionMessage + end +end + +Peer2Backend -[$zmq_inproc]>> Peer2Frontend: PeerIsReadyMessage(Peer1) + +@enduml \ No newline at end of file diff --git a/doc/design/establish_connection/sequence/one_peer_receives_register_peer.png b/doc/design/establish_connection/sequence/one_peer_receives_register_peer.png new file mode 100644 index 0000000000000000000000000000000000000000..2d51109db1aa0419c98805f8edc7e9d0e06f36ac GIT binary patch literal 37619 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 literal 0 HcmV?d00001 diff --git a/doc/design/establish_connection/sequence/one_peer_receives_register_peer.puml b/doc/design/establish_connection/sequence/one_peer_receives_register_peer.puml new file mode 100644 index 00000000..a5c09f04 --- /dev/null +++ b/doc/design/establish_connection/sequence/one_peer_receives_register_peer.puml @@ -0,0 +1,18 @@ +@startuml +'https://plantuml.com/sequence-diagram + +!include base.puml + +[-[$method_call]> Peer1Frontend: register_peer() +Peer1Frontend -[$zmq_inproc]>> Peer1Backend: RegisterPeerMessage +Peer1Backend -[$zmq_tcp_rd_no_con]>> Peer2Backend: SynchronizeConnectionMessage +Peer1Backend <<[$zmq_tcp_rd_no_con]- Peer2Backend : SynchronizeConnectionMessage +par + Peer1Backend -[$zmq_tcp_rd_no_con]>> Peer2Backend: AcknowledgeConnectionMessage + Peer2Backend -[$zmq_inproc]>> Peer2Frontend: PeerIsReadyMessage(Peer1) +else + Peer1Backend <<[$zmq_tcp_rd_no_con]- Peer2Backend: AcknowledgeConnectionMessage + Peer1Frontend <<[$zmq_inproc]- Peer1Backend: PeerIsReadyMessage(Peer2) +end + +@enduml \ No newline at end of file diff --git a/doc/design/establish_connection/state_machine.png b/doc/design/establish_connection/state_machine.png new file mode 100644 index 0000000000000000000000000000000000000000..f9e87b255ea8df437c998d208316c92ecfc22456 GIT binary patch literal 48928 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 B%Rc}B literal 0 HcmV?d00001 diff --git a/doc/design/establish_connection/state_machine.puml b/doc/design/establish_connection/state_machine.puml new file mode 100644 index 00000000..9cfb4189 --- /dev/null +++ b/doc/design/establish_connection/state_machine.puml @@ -0,0 +1,18 @@ +@startuml +'https://plantuml.com/state-diagram + +[*] --> PeerIsRegistered : received RegisterPeerMessage / \n send SynchronizeConnectionMessage +[*] --> ReceivedSynchronizeConnectionMessage : received SynchronizeConnectionMessage / \n send SynchronizeConnectionMessage \n send AcknowledgeConnectionMessage +PeerIsRegistered --> ReceivedSynchronizeConnectionMessage : received SynchronizeConnectionMessage \n send AcknowledgeConnectionMessage +PeerIsRegistered --> ReceivedAcknowledgeConnectionMessage : received AcknowledgeConnectionMessage + +ReceivedSynchronizeConnectionMessage --> PeerIsReady : received AcknowledgeConnectionMessage + +PeerIsRegistered -> PeerIsRegistered : Timeout / \n send SynchronizeConnectionMessage + +ReceivedSynchronizeConnectionMessage --> ReceivedSynchronizeConnectionMessage : Timeout / \n send SynchronizeConnectionMessage +ReceivedSynchronizeConnectionMessage --> PeerIsReady : Waited a while + +ReceivedAcknowledgeConnectionMessage --> PeerIsReady : received SynchronizeConnectionMessage \n send AcknowledgeConnectionMessage +ReceivedAcknowledgeConnectionMessage --> PeerIsReady : Waited a while +@enduml \ No newline at end of file diff --git a/doc/design/legend.puml b/doc/design/legend.puml new file mode 100644 index 00000000..3ed95b6f --- /dev/null +++ b/doc/design/legend.puml @@ -0,0 +1,14 @@ +@startuml +!$udp = '#red' +!$method_call = '#green' +!$zmq_inproc = '#blue' +!$zmq_tcp_rd_no_con = '#darkviolet' + +legend + |Color| Protocol | + |<$udp>| UDP | + |<$method_call>| method calls | + |<$zmq_inproc>| ZMQ inproc | + |<$zmq_tcp_rd_no_con>| ZMQ TCP Dealer/Router Connection Less | +end legend +@enduml \ No newline at end of file diff --git a/doc/design/local_discovery_strategy/both_peer_receive_ping.png b/doc/design/local_discovery_strategy/both_peer_receive_ping.png new file mode 100644 index 0000000000000000000000000000000000000000..fc193faa3e2da0de840db996005692cbdbe3b6ea GIT binary patch literal 38597 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 Peer2DiscoveryStrategy: PingMessage + Peer2DiscoveryStrategy -[$method_call]> Peer2Frontend: register_peer() + return + ref over Peer1Frontend, Peer2Frontend: Establish Connection + Peer2DiscoveryStrategy -[$method_call]> Peer2Frontend: are_all_peers_connected() + return + end +else + loop are_all_peers_connected() + Peer1DiscoveryStrategy <<[$udp]- Peer2DiscoveryStrategy: PingMessage + Peer1DiscoveryStrategy -[$method_call]> Peer1Frontend: register_peer() + return + ref over Peer1Frontend, Peer2Frontend: Establish Connection + Peer1DiscoveryStrategy -[$method_call]> Peer1Frontend: are_all_peers_connected() + return + end +end + +@enduml \ No newline at end of file diff --git a/doc/design/local_discovery_strategy/one_peer_receives_ping.png b/doc/design/local_discovery_strategy/one_peer_receives_ping.png new file mode 100644 index 0000000000000000000000000000000000000000..ef19312a37986cc76f3e849d2bc8632fb8bd5202 GIT binary patch literal 31576 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 bqZM(I#WvQ&2244RU!> Peer2DiscoveryStrategy: PingMessage + Peer2DiscoveryStrategy -[$method_call]> Peer2Frontend: register_peer() + return + ref over Peer1Frontend, Peer2Frontend: Establish Connection + Peer2DiscoveryStrategy -[$method_call]> Peer2Frontend: are_all_peers_connected() + return + end +else + loop are_all_peers_connected() + Peer1DiscoveryStrategy x[$udp]- Peer2DiscoveryStrategy: PingMessage + end +end + +@enduml \ No newline at end of file diff --git a/doc/design/udf_discovery_and_communication.rst b/doc/design/udf_discovery_and_communication.rst index 2b35796a..2f8ae0be 100644 --- a/doc/design/udf_discovery_and_communication.rst +++ b/doc/design/udf_discovery_and_communication.rst @@ -1,80 +1,59 @@ UDF Discovery and Communication =============================== -=============== -Local Discovery -=============== +=================== +Establish Connection +=================== -********* -Overview: -********* +* We use a protocol similar to the TCP Handshake for establishing the connection +* However, our protocol has two different requirements compared to TCP: -- UDP Broadcast for the initial `PingMessage` with connection information for the receiving socket of the reliable network -- After receiving `PingMessage`: + * Two peers can establish the connection at the same time + * In case of lost messages, one of the peers in a connection can successful terminate - - Create sending socket for peer to its receiving socket of the reliable network - - Send a `ReadyToReceiveMessage` with our connection information including the receiving socket port - over our sending socket for it to inform the peer that we are ready to receive from it +* To handle, these two requirements, we add the following modification: -- After receiving `ReadyToReceiveMessage` on our receiving socket: + * We allow both peers to send a synchronize at the same time + * When a peer receives the `SynchronizeConnectionMessage` from the second peer - - If not yet discovered, we create a sending socket for the peer to its receiving socket of the reliable network - - Send a `ReadyToReceiveMessage` with our connection information including the receiving socket port - over our sending socket for it, in case we didn't get its `PingMessage`. - - If we didn't get a `ReadyToReceiveMessage` message after a certain time , - yet, we send a `AreYouReadyToReceiveMessage` to a discovered peer + * It sends first a `SynchronizeConnectionMessage` and `AcknowledgeConnectionMessage` back + * It can mark the second peer as ready, after it waited for the peer_is_ready_wait_time - - This should prevent a stuck handshake if we should lose a `ReadyToReceiveMessage` for whatever reason +* Both peers register each other: -- After receiving `AreYouReadyToReceiveMessage`: +.. image:: establish_connection/sequence/both_peers_receive_register_peer.png - - If not yet discovered, we create a sending socket for the peer to its receiving socket of the reliable network - - Send a `ReadyToReceiveMessage` with our connection information including the receiving socket port - over our sending socket for it, in case we didn't get its `PingMessage`. +* One peer registers the other peer: -.. image:: udf_communication_simple_overview.drawio.png +.. image:: establish_connection/sequence/one_peer_receives_register_peer.png -******** -Details: -******** +* Both peers register each other, one peer loses the `SynchronizeConnectionMessage`: -We separated the Local Discovery into two components. The component `LocalDiscoveryStrategy` implements -the discovery via UDP Broadcast. The component `PeerCommunicator` handles the reliable network. +.. image:: establish_connection/sequence/both_peers_receive_register_peer_one_peer_loses_synchonize.png -If the `LocalDiscoveryStrategy` receives a `PingMessage` via UDP it registers the connection info for -the reliable network of the peer with the `PeerCommunicator`. +* Both peers register each other, both lose the `SynchronizeConnectionMessage`: -The `PeerCommunicator` then handles the reliable network communication. -This includes sending and receiving the `ReadyToReceiveMessage` or `AreYouReadyToReceiveMessage`. -It also provides an interface for the user of the library to check if all peers are connected, which peers are there -and to send and receive message from these peers. +.. image:: establish_connection/sequence/both_peers_receive_register_peer_both_peers_lose_synchonize.png -The `PeerCommunicator` can be used with different discovery strategies. -The `LocalDiscoveryStrategy` is one, but the `GlobalDiscoveryStrategy` can use it as well -to form the reliable networks between the leaders. +* State diagram: -The current implementation of the `PeerCommunicator` use `ZMQ` for the reliable communication, -because it abstracts away the low-level network. It provides: +.. image:: establish_connection/state_diagram.png -- A message-based interface, instead the stream-based interface of TCP. -- Asynchronous message queue, instead of synchronous TCP socket +======================== +Local Discovery Strategy +======================== - - Being asynchronous means that the timings of the physical connection setup and tear down, - reconnect and effective delivery are transparent to the user and organized by ZeroMQ itself. - - Further, messages may be queued in the event that a peer is unavailable to receive them. +- The Local Discovery Strategy sends `PingMessage` with connection information + for establishing the connection via UDP Broadcast. +- When a peer receives a `PingMessage` from another peer. + it registers the other peer and treis to establish a connection +- The strategy sends and receives UDP Broadcast messages until all other peers are connected -We further split up the `PeerCommunicator` into a frontend which is called `PeerCommunicator` -and a `BackgroundListener` which runs in a thread. The `BackgroundListener` is also split into the -`BackgroundListenerInterface` and the `BackgroundListenerThread` to simplify the interaction between it -and the `PeerCommunicator` +* Both peers receive `PingMessage`: -The `BackgroundListenerThread` listens for incoming messages from other peers or the frontend and -forwards messages to the frontend. +.. image:: establish_connection/both_peer_receive_ping.png -Here a detailed overview of the information flow: +* One peer receive `PingMessage`: -.. image:: udf_communication_detail_overview.drawio.png +.. image:: establish_connection/one_peer_receives_ping.png -Here the state machines for the BackgroundListener and Frontend - -.. image:: peer_communicator_state_machine.drawio.png diff --git a/exasol_advanced_analytics_framework/udf_communication/ip_address.py b/exasol_advanced_analytics_framework/udf_communication/ip_address.py index cebde836..02ab2ca6 100644 --- a/exasol_advanced_analytics_framework/udf_communication/ip_address.py +++ b/exasol_advanced_analytics_framework/udf_communication/ip_address.py @@ -1,5 +1,3 @@ -import dataclasses - from pydantic import BaseModel diff --git a/exasol_advanced_analytics_framework/udf_communication/local_discovery_socket.py b/exasol_advanced_analytics_framework/udf_communication/local_discovery_socket.py index 63c4489a..a852cdcf 100644 --- a/exasol_advanced_analytics_framework/udf_communication/local_discovery_socket.py +++ b/exasol_advanced_analytics_framework/udf_communication/local_discovery_socket.py @@ -1,6 +1,4 @@ import socket -import time -from typing import Optional from exasol_advanced_analytics_framework.udf_communication.ip_address import IPAddress, Port diff --git a/exasol_advanced_analytics_framework/udf_communication/messages.py b/exasol_advanced_analytics_framework/udf_communication/messages.py index 35c9a598..dd331895 100644 --- a/exasol_advanced_analytics_framework/udf_communication/messages.py +++ b/exasol_advanced_analytics_framework/udf_communication/messages.py @@ -1,4 +1,4 @@ -from typing import Literal, Union, ForwardRef, List, Optional +from typing import Literal, Union from pydantic import BaseModel @@ -35,16 +35,20 @@ class MyConnectionInfoMessage(BaseModel, frozen=True): my_connection_info: ConnectionInfo -class WeAreReadyToReceiveMessage(BaseModel, frozen=True): - message_type: Literal["WeAreReadyToReceiveMessage"] = "WeAreReadyToReceiveMessage" +class SynchronizeConnectionMessage(BaseModel, frozen=True): + message_type: Literal["SynchronizeConnectionMessage"] = "SynchronizeConnectionMessage" source: ConnectionInfo -class AreYouReadyToReceiveMessage(BaseModel, frozen=True): - message_type: Literal["AreYouReadyToReceiveMessage"] = "AreYouReadyToReceiveMessage" +class AcknowledgeConnectionMessage(BaseModel, frozen=True): + message_type: Literal["AcknowledgeConnectionMessage"] = "AcknowledgeConnectionMessage" source: ConnectionInfo +class TimeoutMessage(BaseModel, frozen=True): + message_type: Literal["TimeoutMessage"] = "TimeoutMessage" + + class Message(BaseModel, frozen=True): __root__: Union[ PingMessage, @@ -52,7 +56,8 @@ class Message(BaseModel, frozen=True): StopMessage, PayloadMessage, MyConnectionInfoMessage, - WeAreReadyToReceiveMessage, - AreYouReadyToReceiveMessage, - PeerIsReadyToReceiveMessage + PeerIsReadyToReceiveMessage, + SynchronizeConnectionMessage, + AcknowledgeConnectionMessage, + TimeoutMessage ] diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/abort_timeout_sender.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/abort_timeout_sender.py new file mode 100644 index 00000000..3be010fb --- /dev/null +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/abort_timeout_sender.py @@ -0,0 +1,51 @@ +import structlog +from structlog.typing import FilteringBoundLogger + +from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo +from exasol_advanced_analytics_framework.udf_communication.messages import TimeoutMessage +from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.timer import Timer +from exasol_advanced_analytics_framework.udf_communication.serialization import serialize_message +from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import Socket + +LOGGER: FilteringBoundLogger = structlog.get_logger() + + +class AbortTimeoutSender: + def __init__(self, + my_connection_info: ConnectionInfo, + peer: Peer, + out_control_socket: Socket, + timer: Timer): + self._timer = timer + self._out_control_socket = out_control_socket + self._finished = False + self._logger = LOGGER.bind( + peer=peer.dict(), + my_connection_info=my_connection_info.dict()) + + def reset_timer(self): + self._logger.info("reset_timer") + self._timer.reset_timer() + + def stop(self): + self._logger.info("stop") + self._finished = True + + def send_if_necessary(self): + self._logger.debug("send_if_necessary") + should_we_send = self._should_we_send() + if should_we_send: + self._finished = True + self._send_timeout_to_frontend() + + def _should_we_send(self): + is_time = self._timer.is_time() + result = is_time and not self._finished + return result + + def _send_timeout_to_frontend(self): + self._logger.debug("send") + message = TimeoutMessage() + serialized_message = serialize_message(message) + self._out_control_socket.send(serialized_message) diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_listener_interface.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_listener_interface.py index 37a09746..ede4c63a 100644 --- a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_listener_interface.py +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_listener_interface.py @@ -1,5 +1,4 @@ import threading -import traceback from typing import Optional, Iterator import structlog @@ -12,6 +11,7 @@ from exasol_advanced_analytics_framework.udf_communication.peer import Peer from exasol_advanced_analytics_framework.udf_communication.peer_communicator.background_listener_thread import \ BackgroundListenerThread +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.clock import Clock from exasol_advanced_analytics_framework.udf_communication.serialization import deserialize_message, serialize_message from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import SocketFactory, \ SocketType, Socket, PollerFlag @@ -25,11 +25,16 @@ def __init__(self, name: str, socket_factory: SocketFactory, listen_ip: IPAddress, - group_identifier: str): + group_identifier: str, + clock: Clock, + poll_timeout_in_ms: int, + synchronize_timeout_in_ms: int, + abort_timeout_in_ms: int, + peer_is_ready_wait_time_in_ms: int, + send_socket_linger_time_in_ms:int, + trace_logging: bool): self._name = name self._logger = LOGGER.bind( - module_name=__name__, - clazz=self.__class__.__name__, name=self._name, group_identifier=group_identifier ) @@ -43,6 +48,13 @@ def __init__(self, group_identifier=group_identifier, out_control_socket_address=out_control_socket_address, in_control_socket_address=in_control_socket_address, + clock=clock, + poll_timeout_in_ms=poll_timeout_in_ms, + synchronize_timeout_in_ms=synchronize_timeout_in_ms, + abort_timeout_in_ms=abort_timeout_in_ms, + peer_is_ready_wait_time_in_ms=peer_is_ready_wait_time_in_ms, + send_socket_linger_time_in_ms=send_socket_linger_time_in_ms, + trace_logging=trace_logging ) self._thread = threading.Thread(target=self._background_listener_run.run) self._thread.daemon = True @@ -70,10 +82,7 @@ def _set_my_connection_info(self): assert isinstance(specific_message_obj, MyConnectionInfoMessage) self._my_connection_info = specific_message_obj.my_connection_info except Exception as e: - self._logger.exception("Exception", - location="_set_my_connection_info", - raw_message=message, - exception=traceback.format_exc()) + self._logger.exception("Exception", raw_message=message) @property def my_connection_info(self) -> ConnectionInfo: @@ -95,13 +104,9 @@ def receive_messages(self, timeout_in_milliseconds: Optional[int] = 0) -> Iterat timeout_in_milliseconds = 0 yield specific_message_obj except Exception as e: - self._logger.exception("Exception", - location="receive_messages", - raw_message=message, - exception=traceback.format_exc()) + self._logger.exception("Exception", raw_message=message) def close(self): - logger = self._logger.bind(location="close") self._logger.info("start") self._send_stop() self._thread.join() @@ -110,5 +115,6 @@ def close(self): self._logger.info("end") def _send_stop(self): + self._logger.info("_send_stop") stop_message = StopMessage() self._in_control_socket.send(serialize_message(stop_message)) diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_listener_thread.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_listener_thread.py index 029ae6ef..aa8ec490 100644 --- a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_listener_thread.py +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_listener_thread.py @@ -1,5 +1,4 @@ import enum -import traceback from typing import Dict, List import structlog @@ -8,10 +7,11 @@ from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo from exasol_advanced_analytics_framework.udf_communication.ip_address import IPAddress, Port from exasol_advanced_analytics_framework.udf_communication.messages import Message, StopMessage, RegisterPeerMessage, \ - WeAreReadyToReceiveMessage, PayloadMessage, MyConnectionInfoMessage, AreYouReadyToReceiveMessage + PayloadMessage, MyConnectionInfoMessage, SynchronizeConnectionMessage, AcknowledgeConnectionMessage from exasol_advanced_analytics_framework.udf_communication.peer import Peer from exasol_advanced_analytics_framework.udf_communication.peer_communicator.background_peer_state import \ BackgroundPeerState +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.clock import Clock from exasol_advanced_analytics_framework.udf_communication.serialization import deserialize_message, serialize_message from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import SocketFactory, \ SocketType, Socket, PollerFlag, Frame @@ -31,20 +31,28 @@ def __init__(self, group_identifier: str, out_control_socket_address: str, in_control_socket_address: str, - poll_timeout_in_seconds: int = 1, - reminder_timeout_in_seconds: float = 1): - self._wait_time_between_reminder_in_seconds = reminder_timeout_in_seconds + clock: Clock, + poll_timeout_in_ms: int, + synchronize_timeout_in_ms: int, + abort_timeout_in_ms: int, + peer_is_ready_wait_time_in_ms: int, + send_socket_linger_time_in_ms: int, + trace_logging: bool): + self._send_socket_linger_time_in_ms = send_socket_linger_time_in_ms + self._trace_logging = trace_logging + self._clock = clock + self._peer_is_ready_wait_time_in_ms = peer_is_ready_wait_time_in_ms + self._abort_timeout_in_ms = abort_timeout_in_ms + self._synchronize_timeout_in_ms = synchronize_timeout_in_ms self._name = name self._logger = LOGGER.bind( - module_name=__name__, - clazz=self.__class__.__name__, name=self._name, group_identifier=group_identifier) self._group_identifier = group_identifier self._listen_ip = listen_ip self._in_control_socket_address = in_control_socket_address self._out_control_socket_address = out_control_socket_address - self._poll_timeout_in_seconds = poll_timeout_in_seconds + self._poll_timeout_in_ms = poll_timeout_in_ms self._socket_factory = socket_factory self._status = BackgroundListenerThread.Status.RUNNING @@ -61,14 +69,13 @@ def run(self): self._close() def _close(self): - logger = self._logger.bind(location="close") - logger.info("start") + self._logger.info("start") self._out_control_socket.close(linger=0) self._in_control_socket.close(linger=0) for peer_state in self._peer_state.values(): peer_state.close() self._listener_socket.close(linger=0) - logger.info("end") + self._logger.info("end") def _create_listener_socket(self): self._listener_socket: Socket = self._socket_factory.create_socket(SocketType.ROUTER) @@ -90,10 +97,9 @@ def _create_poller(self): self.poller.register(self._listener_socket, flags=PollerFlag.POLLIN) def _run_message_loop(self): - log = self._logger.bind(location="_run_message_loop") try: while self._status == BackgroundListenerThread.Status.RUNNING: - poll = self.poller.poll(timeout_in_ms=self._poll_timeout_in_seconds * 1000) + poll = self.poller.poll(timeout_in_ms=self._poll_timeout_in_ms) if self._in_control_socket in poll and PollerFlag.POLLIN in poll[self._in_control_socket]: message = self._in_control_socket.receive() self._status = self._handle_control_message(message) @@ -102,12 +108,12 @@ def _run_message_loop(self): self._handle_listener_message(message) if self._status == BackgroundListenerThread.Status.RUNNING: for peer_state in self._peer_state.values(): - peer_state._send_are_you_ready_to_receive_if_necassary() + peer_state.resend_if_necessary() except Exception as e: - log.exception("Exception", exception=traceback.format_exc()) + self._logger.exception("Exception in message loop") + def _handle_control_message(self, message: bytes) -> Status: - logger = self._logger.bind(location="_handle_control_message") try: message_obj: Message = deserialize_message(message, Message) specific_message_obj = message_obj.__root__ @@ -116,63 +122,62 @@ def _handle_control_message(self, message: bytes) -> Status: elif isinstance(specific_message_obj, RegisterPeerMessage): self._add_peer(specific_message_obj.peer) else: - logger.error( - "Unknown message type", - message=specific_message_obj.dict()) + self._logger.error("Unknown message type", message=specific_message_obj.dict()) except Exception as e: - logger.exception( - "Could not deserialize message", - message=message, - exception=traceback.format_exc() - ) + self._logger.exception("Exception during handling message", message=message) return BackgroundListenerThread.Status.RUNNING def _add_peer(self, peer): + if peer.connection_info.group_identifier != self._my_connection_info.group_identifier: + self._logger.error("Peer belongs to a different group", + my_connection_info=self._my_connection_info.dict(), + peer=peer.dict()) + raise ValueError("Peer belongs to a different group") if peer not in self._peer_state: - self._peer_state[peer] = BackgroundPeerState( + self._peer_state[peer] = BackgroundPeerState.create( my_connection_info=self._my_connection_info, out_control_socket=self._out_control_socket, socket_factory=self._socket_factory, peer=peer, - reminder_timeout_in_seconds=self._wait_time_between_reminder_in_seconds + clock=self._clock, + peer_is_ready_wait_time_in_ms=self._peer_is_ready_wait_time_in_ms, + abort_timeout_in_ms=self._abort_timeout_in_ms, + synchronize_timeout_in_ms=self._synchronize_timeout_in_ms, + send_socket_linger_time_in_ms=self._send_socket_linger_time_in_ms ) def _handle_listener_message(self, message: List[Frame]): logger = self._logger.bind( - location="_handle_listener_message", sender=message[0].to_bytes() ) + message_content_bytes = message[1].to_bytes() try: - message_obj: Message = deserialize_message(message[1].to_bytes(), Message) + message_obj: Message = deserialize_message(message_content_bytes, Message) specific_message_obj = message_obj.__root__ - if isinstance(specific_message_obj, WeAreReadyToReceiveMessage): - self._handle_we_are_ready_to_receive(specific_message_obj) - elif isinstance(specific_message_obj, AreYouReadyToReceiveMessage): - self._handle_are_you_ready_to_receive(specific_message_obj) + if isinstance(specific_message_obj, SynchronizeConnectionMessage): + self._handle_synchronize_connection(specific_message_obj) + elif isinstance(specific_message_obj, AcknowledgeConnectionMessage): + self._handle_acknowledge_connection(specific_message_obj) elif isinstance(specific_message_obj, PayloadMessage): self._handle_payload_message(specific_message_obj, message) else: logger.error("Unknown message type", message=specific_message_obj.dict()) except Exception as e: - logger.exception( - "Could not deserialize message", - message=message[1].to_bytes(), - exception=traceback.format_exc() - ) + logger.exception("Exception during handling message", message_content=message_content_bytes) def _handle_payload_message(self, message: PayloadMessage, frames: List[Frame]): peer = Peer(connection_info=message.source) self._peer_state[peer].forward_payload(frames[2:]) - def _handle_we_are_ready_to_receive(self, message: WeAreReadyToReceiveMessage): + def _handle_synchronize_connection(self, message: SynchronizeConnectionMessage): peer = Peer(connection_info=message.source) self._add_peer(peer) - self._peer_state[peer].received_peer_is_ready_to_receive() + self._peer_state[peer].received_synchronize_connection() - def _handle_are_you_ready_to_receive(self, message: AreYouReadyToReceiveMessage): + def _handle_acknowledge_connection(self, message: AcknowledgeConnectionMessage): peer = Peer(connection_info=message.source) self._add_peer(peer) - self._peer_state[peer].received_are_you_ready_to_receive() + self._peer_state[peer].received_acknowledge_connection() def _set_my_connection_info(self, port: int): self._my_connection_info = ConnectionInfo( diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_peer_state.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_peer_state.py index a9b42bc0..ce797f23 100644 --- a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_peer_state.py +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/background_peer_state.py @@ -1,105 +1,120 @@ -import contextlib -import time -from typing import Optional, Generator, List +from typing import List import structlog from structlog.typing import FilteringBoundLogger from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo -from exasol_advanced_analytics_framework.udf_communication.messages import Message, WeAreReadyToReceiveMessage, \ - AreYouReadyToReceiveMessage, PeerIsReadyToReceiveMessage +from exasol_advanced_analytics_framework.udf_communication.messages import Message, AcknowledgeConnectionMessage from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.abort_timeout_sender import \ + AbortTimeoutSender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.clock import Clock from exasol_advanced_analytics_framework.udf_communication.peer_communicator.get_peer_receive_socket_name import \ get_peer_receive_socket_name -from exasol_advanced_analytics_framework.udf_communication.serialization import serialize_message -from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import SocketFactory, \ - SocketType, Socket, Frame +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.peer_is_ready_sender import \ + PeerIsReadySender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.sender import Sender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.synchronize_connection_sender import \ + SynchronizeConnectionSender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.timer import Timer +from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import Socket, SocketFactory, \ + SocketType, Frame -LOGGER: FilteringBoundLogger = structlog.get_logger(__name__) +LOGGER: FilteringBoundLogger = structlog.get_logger() class BackgroundPeerState: + @classmethod + def create( + cls, + my_connection_info: ConnectionInfo, + out_control_socket: Socket, + socket_factory: SocketFactory, + peer: Peer, + clock: Clock, + synchronize_timeout_in_ms: int, + abort_timeout_in_ms: int, + peer_is_ready_wait_time_in_ms: int, + send_socket_linger_time_in_ms: int + ): + sender = Sender(my_connection_info=my_connection_info, + socket_factory=socket_factory, + peer=peer, + send_socket_linger_time_in_ms=send_socket_linger_time_in_ms) + synchronize_connection_sender = SynchronizeConnectionSender( + my_connection_info=my_connection_info, + peer=peer, + sender=sender, + timer=Timer(clock=clock, timeout_in_ms=synchronize_timeout_in_ms) + ) + abort_timeout_sender = AbortTimeoutSender( + out_control_socket=out_control_socket, + timer=Timer(clock=clock, timeout_in_ms=abort_timeout_in_ms), + my_connection_info=my_connection_info, + peer=peer + ) + peer_is_ready_sender = PeerIsReadySender( + out_control_socket=out_control_socket, + timer=Timer(clock=clock, timeout_in_ms=peer_is_ready_wait_time_in_ms), + peer=peer, + my_connection_info=my_connection_info, + ) + peer_state = cls( + my_connection_info=my_connection_info, + socket_factory=socket_factory, + peer=peer, + sender=sender, + synchronize_connection_sender=synchronize_connection_sender, + abort_timeout_sender=abort_timeout_sender, + peer_is_ready_sender=peer_is_ready_sender + ) + return peer_state + def __init__(self, my_connection_info: ConnectionInfo, - out_control_socket: Socket, socket_factory: SocketFactory, peer: Peer, - reminder_timeout_in_seconds: float = 1): - self._out_control_socket = out_control_socket + sender: Sender, + synchronize_connection_sender: SynchronizeConnectionSender, + abort_timeout_sender: AbortTimeoutSender, + peer_is_ready_sender: PeerIsReadySender): self._my_connection_info = my_connection_info - self._wait_time_between_reminder_in_seconds = reminder_timeout_in_seconds self._peer = peer self._socket_factory = socket_factory - self._peer_can_receive_from_us = False - self._last_send_ready_to_receive_timestamp_in_seconds: Optional[float] = None - self._logger = LOGGER.bind( - module_name=__name__, - clazz=self.__class__.__name__, - peer=self._peer, - my_connection_info=self._my_connection_info, - ) self._create_receive_socket() - self._send_we_are_ready_to_receive() + self._sender = sender + self._synchronize_connection_sender = synchronize_connection_sender + self._abort_timeout_sender = abort_timeout_sender + self._peer_is_ready_sender = peer_is_ready_sender + self._synchronize_connection_sender.send_if_necessary(force=True) + self._logger = LOGGER.bind( + peer=self._peer.dict(), + my_connection_info=self._my_connection_info.dict()) def _create_receive_socket(self): self._receive_socket = self._socket_factory.create_socket(SocketType.PAIR) receive_socket_address = get_peer_receive_socket_name(self._peer) self._receive_socket.bind(receive_socket_address) - @contextlib.contextmanager - def _create_send_socket(self) -> Generator[Socket, None, None]: - send_socket: Socket - with self._socket_factory.create_socket(SocketType.DEALER) as send_socket: - send_socket.connect( - f"tcp://{self._peer.connection_info.ipaddress.ip_address}:{self._peer.connection_info.port.port}") - yield send_socket - - def _is_time_to_send_are_you_ready_to_receive(self): - current_timestamp_in_seconds = time.monotonic() - if self._last_send_ready_to_receive_timestamp_in_seconds is not None: - diff = current_timestamp_in_seconds - self._last_send_ready_to_receive_timestamp_in_seconds - if diff > self._wait_time_between_reminder_in_seconds: - self._last_send_ready_to_receive_timestamp_in_seconds = current_timestamp_in_seconds - return True - else: - self._last_send_ready_to_receive_timestamp_in_seconds = current_timestamp_in_seconds - return False - - def _send_are_you_ready_to_receive_if_necassary(self): - if not self._peer_can_receive_from_us: - if self._is_time_to_send_are_you_ready_to_receive(): - self._logger.info("Send AreYouReadyToReceiveMessage", peer=self._peer, - my_connection_info=self._my_connection_info) - message = Message(__root__=AreYouReadyToReceiveMessage(source=self._my_connection_info)) - self._send(message) - - def _send_we_are_ready_to_receive(self): - message = Message(__root__=WeAreReadyToReceiveMessage(source=self._my_connection_info)) - self._send(message) - - def received_peer_is_ready_to_receive(self): - self._handle_peer_is_ready_to_receive() - - def received_are_you_ready_to_receive(self): - self._handle_peer_is_ready_to_receive() - self._send_we_are_ready_to_receive() - - def _handle_peer_is_ready_to_receive(self): - if not self._peer_can_receive_from_us: - self._send_peer_is_ready_to_frontend() - self._peer_can_receive_from_us = True - - def _send(self, message: Message): - send_socket: Socket - with self._create_send_socket() as send_socket: - serialized_message = serialize_message(message.__root__) - send_socket.send(serialized_message) - - def _send_peer_is_ready_to_frontend(self): - message = PeerIsReadyToReceiveMessage(peer=self._peer) - serialized_message = serialize_message(message) - self._out_control_socket.send(serialized_message) + def resend_if_necessary(self): + self._logger.debug("resend_if_necessary") + self._synchronize_connection_sender.send_if_necessary() + self._abort_timeout_sender.send_if_necessary() + self._peer_is_ready_sender.send_if_necessary() + + def received_synchronize_connection(self): + self._logger.debug("received_synchronize_connection") + self._peer_is_ready_sender.enable() + self._peer_is_ready_sender.reset_timer() + self._abort_timeout_sender.stop() + self._sender.send(Message(__root__=AcknowledgeConnectionMessage(source=self._my_connection_info))) + + def received_acknowledge_connection(self): + self._logger.debug("received_acknowledge_connection") + self._abort_timeout_sender.stop() + self._synchronize_connection_sender.stop() + self._peer_is_ready_sender.send_if_necessary(force=True) def forward_payload(self, frames: List[Frame]): self._receive_socket.send_multipart(frames) diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/clock.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/clock.py new file mode 100644 index 00000000..6f7b6962 --- /dev/null +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/clock.py @@ -0,0 +1,7 @@ +import time + + +class Clock(): + def current_timestamp_in_ms(self) -> int: + timestamp = time.monotonic_ns() // 10 ** 6 + return timestamp diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/get_peer_receive_socket_name.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/get_peer_receive_socket_name.py index f6a2c69b..dc6e5cf4 100644 --- a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/get_peer_receive_socket_name.py +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/get_peer_receive_socket_name.py @@ -5,6 +5,6 @@ def get_peer_receive_socket_name(peer: Peer) -> str: quoted_ip_address = urllib.parse.quote_plus(peer.connection_info.ipaddress.ip_address) - quoted_port = urllib.parse.quote_plus(str(peer.connection_info.port)) + quoted_port = urllib.parse.quote_plus(str(peer.connection_info.port.port)) quoted_group_identifier = peer.connection_info.group_identifier return f"inproc://peer/{quoted_group_identifier}/{quoted_ip_address}/{quoted_port}" diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/peer_communicator.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/peer_communicator.py index 7adad92b..66398e5a 100644 --- a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/peer_communicator.py +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/peer_communicator.py @@ -6,10 +6,11 @@ from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo from exasol_advanced_analytics_framework.udf_communication.ip_address import IPAddress -from exasol_advanced_analytics_framework.udf_communication.messages import PeerIsReadyToReceiveMessage +from exasol_advanced_analytics_framework.udf_communication.messages import PeerIsReadyToReceiveMessage, TimeoutMessage from exasol_advanced_analytics_framework.udf_communication.peer import Peer from exasol_advanced_analytics_framework.udf_communication.peer_communicator.background_listener_interface import \ BackgroundListenerInterface +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.clock import Clock from exasol_advanced_analytics_framework.udf_communication.peer_communicator.frontend_peer_state import \ FrontendPeerState from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import SocketFactory, \ @@ -36,35 +37,50 @@ def __init__(self, number_of_peers: int, listen_ip: IPAddress, group_identifier: str, - socket_factory: SocketFactory): + socket_factory: SocketFactory, + poll_timeout_in_ms: int = 500, + synchronize_timeout_in_ms: int = 1000, + abort_timeout_in_ms: int = 120000, + peer_is_ready_wait_time_in_ms: int = 2000, + send_socket_linger_time_in_ms: int = 100, + clock: Clock = Clock(), + trace_logging: bool = False): self._socket_factory = socket_factory self._name = name - self._log_info = dict(module_name=__name__, - clazz=self.__class__.__name__, - name=self._name, - group_identifier=group_identifier) - self._logger = LOGGER.bind(**self._log_info) + self._logger = LOGGER.bind( + name=self._name, + group_identifier=group_identifier + ) self._number_of_peers = number_of_peers self._background_listener = BackgroundListenerInterface( name=self._name, socket_factory=self._socket_factory, listen_ip=listen_ip, - group_identifier=group_identifier) + group_identifier=group_identifier, + clock=clock, + poll_timeout_in_ms=poll_timeout_in_ms, + synchronize_timeout_in_ms=synchronize_timeout_in_ms, + abort_timeout_in_ms=abort_timeout_in_ms, + peer_is_ready_wait_time_in_ms=peer_is_ready_wait_time_in_ms, + send_socket_linger_time_in_ms=send_socket_linger_time_in_ms, + trace_logging=trace_logging + ) self._my_connection_info = self._background_listener.my_connection_info self._peer_states: Dict[Peer, FrontendPeerState] = {} def _handle_messages(self, timeout_in_milliseconds: Optional[int] = 0): - if not self._are_all_peers_connected(): - for message in self._background_listener.receive_messages(timeout_in_milliseconds): - if isinstance(message, PeerIsReadyToReceiveMessage): - peer = message.peer - self._add_peer_state(peer) - self._peer_states[peer].received_peer_is_ready_to_receive() - else: - self._logger.error( - "Unknown message", - location="_handle_messages", - message=message.dict()) + if self._are_all_peers_connected(): + return + + for message in self._background_listener.receive_messages(timeout_in_milliseconds): + if isinstance(message, PeerIsReadyToReceiveMessage): + peer = message.peer + self._add_peer_state(peer) + self._peer_states[peer].received_peer_is_ready_to_receive() + elif isinstance(message, TimeoutMessage): + raise TimeoutError() + else: + self._logger.error("Unknown message", message=message.dict()) def _add_peer_state(self, peer): if peer not in self._peer_states: diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/peer_is_ready_sender.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/peer_is_ready_sender.py new file mode 100644 index 00000000..0d7eda6d --- /dev/null +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/peer_is_ready_sender.py @@ -0,0 +1,63 @@ +import structlog +from structlog.typing import FilteringBoundLogger + +from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo +from exasol_advanced_analytics_framework.udf_communication.messages import PeerIsReadyToReceiveMessage +from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.timer import Timer +from exasol_advanced_analytics_framework.udf_communication.serialization import serialize_message +from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import Socket + +LOGGER: FilteringBoundLogger = structlog.get_logger() + + +import enum + + +class _State(enum.IntFlag): + Init = enum.auto() + Enabled = enum.auto() + Finished = enum.auto() + + +class PeerIsReadySender: + + + def __init__(self, + out_control_socket: Socket, + peer: Peer, + my_connection_info: ConnectionInfo, + timer: Timer): + self._timer = timer + self._peer = peer + self._out_control_socket = out_control_socket + self._state = _State.Init + self._logger = LOGGER.bind( + peer=self._peer.dict(), + my_connection_info=my_connection_info.dict()) + + def enable(self): + self._logger.debug("enable") + self._state |= _State.Enabled + + def reset_timer(self): + self._logger.debug("reset_timer") + self._timer.reset_timer() + + def send_if_necessary(self, force=False): + self._logger.debug("send_if_necessary") + should_we_send = self._should_we_send() + if should_we_send or force: + self._state |= _State.Finished + self._send_peer_is_ready_to_frontend() + + def _should_we_send(self): + is_time = self._timer.is_time() + result = is_time and (_State.Finished not in self._state) and (_State.Enabled in self._state) + return result + + def _send_peer_is_ready_to_frontend(self): + self._logger.debug("send") + message = PeerIsReadyToReceiveMessage(peer=self._peer) + serialized_message = serialize_message(message) + self._out_control_socket.send(serialized_message) diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/sender.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/sender.py new file mode 100644 index 00000000..025652f7 --- /dev/null +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/sender.py @@ -0,0 +1,49 @@ +from typing import Optional + +import structlog +from structlog.typing import FilteringBoundLogger + +from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo +from exasol_advanced_analytics_framework.udf_communication.messages import Message +from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.serialization import serialize_message +from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import SocketFactory, \ + Socket, SocketType + +LOGGER: FilteringBoundLogger = structlog.get_logger(__name__) + + +class Sender: + def __init__(self, + my_connection_info: ConnectionInfo, + socket_factory: SocketFactory, + peer: Peer, + send_socket_linger_time_in_ms: int): + self._send_socket_linger_time_in_ms = send_socket_linger_time_in_ms + self._my_connection_info = my_connection_info + self._peer = peer + self._socket_factory = socket_factory + + self._logger = LOGGER.bind( + peer=self._peer, + my_connection_info=self._my_connection_info, + ) + + def create_send_socket(self) -> Socket: + send_socket: Optional[Socket] = None + try: + send_socket = self._socket_factory.create_socket(SocketType.DEALER) + send_socket.connect( + "tcp://{ip}:{port}".format( + ip=self._peer.connection_info.ipaddress.ip_address, + port=self._peer.connection_info.port.port + )) + return send_socket + except Exception: + send_socket.close() + + def send(self, message: Message): + with self.create_send_socket() as send_socket: + serialized_message = serialize_message(message.__root__) + send_socket.send(serialized_message) + send_socket.close(self._send_socket_linger_time_in_ms) diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/synchronize_connection_sender.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/synchronize_connection_sender.py new file mode 100644 index 00000000..ba57f9fa --- /dev/null +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/synchronize_connection_sender.py @@ -0,0 +1,46 @@ +import structlog +from structlog.typing import FilteringBoundLogger + +from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo +from exasol_advanced_analytics_framework.udf_communication.messages import Message, SynchronizeConnectionMessage +from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.sender import Sender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.timer import Timer + +LOGGER: FilteringBoundLogger = structlog.get_logger() + + +class SynchronizeConnectionSender(): + def __init__(self, + my_connection_info: ConnectionInfo, + peer: Peer, + sender: Sender, + timer: Timer): + self._my_connection_info = my_connection_info + self._timer = timer + self._sender = sender + self._finished = False + self._logger = LOGGER.bind( + peer=peer.dict(), + my_connection_info=my_connection_info.dict()) + + def stop(self): + self._logger.debug("stop") + self._finished = True + + def send_if_necessary(self, force=False): + self._logger.debug("send_if_necessary") + should_we_send = self._should_we_send() + if should_we_send or force: + self._send() + self._timer.reset_timer() + + def _send(self): + self._logger.debug("send") + message = Message(__root__=SynchronizeConnectionMessage(source=self._my_connection_info)) + self._sender.send(message) + + def _should_we_send(self): + is_time = self._timer.is_time() + result = is_time and not self._finished + return result diff --git a/exasol_advanced_analytics_framework/udf_communication/peer_communicator/timer.py b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/timer.py new file mode 100644 index 00000000..e99e56ce --- /dev/null +++ b/exasol_advanced_analytics_framework/udf_communication/peer_communicator/timer.py @@ -0,0 +1,19 @@ +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.clock import Clock + + +class Timer: + + def __init__(self, + clock: Clock, + timeout_in_ms: int): + self._timeout_in_ms = timeout_in_ms + self._clock = clock + self._last_send_timestamp_in_ms = clock.current_timestamp_in_ms() + + def reset_timer(self): + self._last_send_timestamp_in_ms = self._clock.current_timestamp_in_ms() + + def is_time(self): + current_timestamp_in_ms = self._clock.current_timestamp_in_ms() + diff = current_timestamp_in_ms - self._last_send_timestamp_in_ms + return diff > self._timeout_in_ms diff --git a/exasol_advanced_analytics_framework/udf_communication/socket_factory/fault_injection.py b/exasol_advanced_analytics_framework/udf_communication/socket_factory/fault_injection.py index ea0a616b..1fcc3a37 100644 --- a/exasol_advanced_analytics_framework/udf_communication/socket_factory/fault_injection.py +++ b/exasol_advanced_analytics_framework/udf_communication/socket_factory/fault_injection.py @@ -32,8 +32,6 @@ def __init__(self, internal_socket: abstract.Socket, send_fault_probability: flo raise ValueError( f"send_fault_probability needs to be between 0 and 1 (exclusive) was {send_fault_probability}.") self._logger = LOGGER.bind( - module_name=__name__, - clazz=self.__class__.__name__, socket=str(self) ) self._send_fault_probability = send_fault_probability diff --git a/poetry.lock b/poetry.lock index d2f15a18..5f0f2c4b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,40 @@ # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +[[package]] +name = "bcrypt" +version = "4.0.1" +description = "Modern password hashing for your software and your servers" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "certifi" version = "2023.5.7" @@ -390,6 +425,18 @@ files = [ {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, ] +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + [[package]] name = "dill" version = "0.3.6" @@ -405,6 +452,39 @@ files = [ [package.extras] graph = ["objgraph (>=1.7.2)"] +[[package]] +name = "docker" +version = "6.1.3" +description = "A Python library for the Docker Engine API." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"}, + {file = "docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20"}, +] + +[package.dependencies] +packaging = ">=14.0" +requests = ">=2.26.0" +urllib3 = ">=1.26.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.3)"] + +[[package]] +name = "docutils" +version = "0.20.1" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, +] + [[package]] name = "exasol-bucketfs" version = "0.6.0" @@ -450,6 +530,39 @@ url = "https://github.com/exasol/data-science-utils-python.git" reference = "main" resolved_reference = "24bf0f86eab327c69d371714c9d5edf73adadf2d" +[[package]] +name = "exasol-integration-test-docker-environment" +version = "1.7.1" +description = "Integration Test Docker Environment for Exasol" +category = "dev" +optional = false +python-versions = ">=3.8,<4" +files = [ + {file = "exasol_integration_test_docker_environment-1.7.1-py3-none-any.whl", hash = "sha256:531bf53a5c60c422850472710d3ad11983d8636cb5edfbb705c86f0a4a69a125"}, + {file = "exasol_integration_test_docker_environment-1.7.1.tar.gz", hash = "sha256:7fb6b2e225673c124e1ebecd1bbc03d313bf56d3915d526496f21ba0ca9fa816"}, +] + +[package.dependencies] +click = ">=7.0" +docker = {version = ">=4.0.0", markers = "sys_platform != \"win32\""} +exasol-bucketfs = ">=0.6.0,<2.0.0" +fabric = ">=3.0.1,<4.0.0" +gitpython = ">=2.1.0" +humanfriendly = ">=4.18" +importlib_resources = ">=5.4.0" +jinja2 = ">=2.10.1" +jsonpickle = ">=1.1" +luigi = ">=2.8.4" +netaddr = ">=0.7.19" +networkx = ">=2.3" +portalocker = ">=2.7.0,<3.0.0" +pydot = ">=1.4.0" +pyexasol = ">=0.25.2,<0.26.0" +pytest = ">=7.2.2,<8.0.0" +requests = ">=2.21.0" +simplejson = ">=3.16.0" +"stopwatch.py" = ">=1.0.0" + [[package]] name = "exasol-udf-mock-python" version = "0.1.0" @@ -473,19 +586,39 @@ resolved_reference = "2088d62a7457fd78d8152a2181d459d9d6bd4b0d" [[package]] name = "exceptiongroup" -version = "1.1.1" +version = "1.1.2" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, - {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, ] [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "fabric" +version = "3.1.0" +description = "High level SSH command execution" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "fabric-3.1.0-py3-none-any.whl", hash = "sha256:0a13217db1aa203167376119b0e165081c5906c31e2b2104410685d1310ef8fb"}, + {file = "fabric-3.1.0.tar.gz", hash = "sha256:ea1c5ea3956d196b5990ba720cc8ee457fa1b9c6f265ab3b643ff63b05e8970a"}, +] + +[package.dependencies] +decorator = ">=5" +invoke = ">=2.0" +paramiko = ">=2.4" + +[package.extras] +pytest = ["pytest (>=7)"] + [[package]] name = "fonttools" version = "4.40.0" @@ -544,6 +677,51 @@ ufo = ["fs (>=2.2.0,<3)"] unicode = ["unicodedata2 (>=15.0.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] +[[package]] +name = "gitdb" +version = "4.0.10" +description = "Git Object Database" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.10-py3-none-any.whl", hash = "sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7"}, + {file = "gitdb-4.0.10.tar.gz", hash = "sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.31" +description = "GitPython is a Python library used to interact with Git repositories" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.31-py3-none-any.whl", hash = "sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d"}, + {file = "GitPython-3.1.31.tar.gz", hash = "sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + [[package]] name = "idna" version = "3.4" @@ -587,6 +765,18 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "invoke" +version = "2.1.3" +description = "Pythonic task execution" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "invoke-2.1.3-py3-none-any.whl", hash = "sha256:51e86a08d964160e01c44eccd22f50b25842bd96a9c63c11177032594cb86740"}, + {file = "invoke-2.1.3.tar.gz", hash = "sha256:a3b15d52d50bbabd851b8a39582c772180b614000fa1612b4d92484d54d38c6b"}, +] + [[package]] name = "jinja2" version = "3.1.2" @@ -607,14 +797,14 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "joblib" -version = "1.2.0" +version = "1.3.1" description = "Lightweight pipelining with Python functions" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "joblib-1.2.0-py3-none-any.whl", hash = "sha256:091138ed78f800342968c523bdde947e7a305b8594b910a0fea2ab83c3c6d385"}, - {file = "joblib-1.2.0.tar.gz", hash = "sha256:e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018"}, + {file = "joblib-1.3.1-py3-none-any.whl", hash = "sha256:89cf0529520e01b3de7ac7b74a8102c90d16d54c64b5dd98cafcd14307fdf915"}, + {file = "joblib-1.3.1.tar.gz", hash = "sha256:1f937906df65329ba98013dc9692fe22a4c5e4a648112de500508b18a21b41e3"}, ] [[package]] @@ -712,6 +902,40 @@ files = [ {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, ] +[[package]] +name = "lockfile" +version = "0.12.2" +description = "Platform-independent file locking module" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, + {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, +] + +[[package]] +name = "luigi" +version = "3.3.0" +description = "Workflow mgmgt + task scheduling + dependency resolution." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "luigi-3.3.0.tar.gz", hash = "sha256:cc8642deb6e22f0601ee8df8ad3de639a81521b7e663a7a5044919185ad2b357"}, +] + +[package.dependencies] +python-daemon = "*" +python-dateutil = ">=2.7.5,<3" +tenacity = ">=8,<9" +tornado = ">=5.0,<7" + +[package.extras] +jsonschema = ["jsonschema"] +prometheus = ["prometheus-client (>=0.5,<0.15)"] +toml = ["toml (<2.0.0)"] + [[package]] name = "markupsafe" version = "2.1.3" @@ -860,42 +1084,73 @@ setuptools = "*" docs = ["mkdocs"] testing = ["pytest"] +[[package]] +name = "netaddr" +version = "0.8.0" +description = "A network address manipulation library for Python" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"}, + {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"}, +] + +[[package]] +name = "networkx" +version = "3.1" +description = "Python package for creating and manipulating graphs and networks" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "networkx-3.1-py3-none-any.whl", hash = "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36"}, + {file = "networkx-3.1.tar.gz", hash = "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61"}, +] + +[package.extras] +default = ["matplotlib (>=3.4)", "numpy (>=1.20)", "pandas (>=1.3)", "scipy (>=1.8)"] +developer = ["mypy (>=1.1)", "pre-commit (>=3.2)"] +doc = ["nb2plots (>=0.6)", "numpydoc (>=1.5)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.13)", "sphinx (>=6.1)", "sphinx-gallery (>=0.12)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.10)", "sympy (>=1.10)"] +test = ["codecov (>=2.1)", "pytest (>=7.2)", "pytest-cov (>=4.0)"] + [[package]] name = "numpy" -version = "1.24.3" +version = "1.24.4" description = "Fundamental package for array computing in Python" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "numpy-1.24.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c1104d3c036fb81ab923f507536daedc718d0ad5a8707c6061cdfd6d184e570"}, - {file = "numpy-1.24.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:202de8f38fc4a45a3eea4b63e2f376e5f2dc64ef0fa692838e31a808520efaf7"}, - {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8535303847b89aa6b0f00aa1dc62867b5a32923e4d1681a35b5eef2d9591a463"}, - {file = "numpy-1.24.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d926b52ba1367f9acb76b0df6ed21f0b16a1ad87c6720a1121674e5cf63e2b6"}, - {file = "numpy-1.24.3-cp310-cp310-win32.whl", hash = "sha256:f21c442fdd2805e91799fbe044a7b999b8571bb0ab0f7850d0cb9641a687092b"}, - {file = "numpy-1.24.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f23af8c16022663a652d3b25dcdc272ac3f83c3af4c02eb8b824e6b3ab9d7"}, - {file = "numpy-1.24.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a7721ec204d3a237225db3e194c25268faf92e19338a35f3a224469cb6039a3"}, - {file = "numpy-1.24.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6cc757de514c00b24ae8cf5c876af2a7c3df189028d68c0cb4eaa9cd5afc2bf"}, - {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76e3f4e85fc5d4fd311f6e9b794d0c00e7002ec122be271f2019d63376f1d385"}, - {file = "numpy-1.24.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1d3c026f57ceaad42f8231305d4653d5f05dc6332a730ae5c0bea3513de0950"}, - {file = "numpy-1.24.3-cp311-cp311-win32.whl", hash = "sha256:c91c4afd8abc3908e00a44b2672718905b8611503f7ff87390cc0ac3423fb096"}, - {file = "numpy-1.24.3-cp311-cp311-win_amd64.whl", hash = "sha256:5342cf6aad47943286afa6f1609cad9b4266a05e7f2ec408e2cf7aea7ff69d80"}, - {file = "numpy-1.24.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7776ea65423ca6a15255ba1872d82d207bd1e09f6d0894ee4a64678dd2204078"}, - {file = "numpy-1.24.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ae8d0be48d1b6ed82588934aaaa179875e7dc4f3d84da18d7eae6eb3f06c242c"}, - {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecde0f8adef7dfdec993fd54b0f78183051b6580f606111a6d789cd14c61ea0c"}, - {file = "numpy-1.24.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4749e053a29364d3452c034827102ee100986903263e89884922ef01a0a6fd2f"}, - {file = "numpy-1.24.3-cp38-cp38-win32.whl", hash = "sha256:d933fabd8f6a319e8530d0de4fcc2e6a61917e0b0c271fded460032db42a0fe4"}, - {file = "numpy-1.24.3-cp38-cp38-win_amd64.whl", hash = "sha256:56e48aec79ae238f6e4395886b5eaed058abb7231fb3361ddd7bfdf4eed54289"}, - {file = "numpy-1.24.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4719d5aefb5189f50887773699eaf94e7d1e02bf36c1a9d353d9f46703758ca4"}, - {file = "numpy-1.24.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ec87a7084caa559c36e0a2309e4ecb1baa03b687201d0a847c8b0ed476a7187"}, - {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea8282b9bcfe2b5e7d491d0bf7f3e2da29700cec05b49e64d6246923329f2b02"}, - {file = "numpy-1.24.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210461d87fb02a84ef243cac5e814aad2b7f4be953b32cb53327bb49fd77fbb4"}, - {file = "numpy-1.24.3-cp39-cp39-win32.whl", hash = "sha256:784c6da1a07818491b0ffd63c6bbe5a33deaa0e25a20e1b3ea20cf0e43f8046c"}, - {file = "numpy-1.24.3-cp39-cp39-win_amd64.whl", hash = "sha256:d5036197ecae68d7f491fcdb4df90082b0d4960ca6599ba2659957aafced7c17"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:352ee00c7f8387b44d19f4cada524586f07379c0d49270f87233983bc5087ca0"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7d6acc2e7524c9955e5c903160aa4ea083736fde7e91276b0e5d98e6332812"}, - {file = "numpy-1.24.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:35400e6a8d102fd07c71ed7dcadd9eb62ee9a6e84ec159bd48c28235bbb0f8e4"}, - {file = "numpy-1.24.3.tar.gz", hash = "sha256:ab344f1bf21f140adab8e47fdbc7c35a477dc01408791f8ba00d018dd0bc5155"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, ] [[package]] @@ -959,6 +1214,28 @@ pytz = ">=2020.1" [package.extras] test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] +[[package]] +name = "paramiko" +version = "3.2.0" +description = "SSH2 protocol library" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "paramiko-3.2.0-py3-none-any.whl", hash = "sha256:df0f9dd8903bc50f2e10580af687f3015bf592a377cd438d2ec9546467a14eb8"}, + {file = "paramiko-3.2.0.tar.gz", hash = "sha256:93cdce625a8a1dc12204439d45033f3261bdb2c201648cfcdc06f9fd0f94ec29"}, +] + +[package.dependencies] +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" + +[package.extras] +all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=2.0)"] + [[package]] name = "pastel" version = "0.2.1" @@ -973,78 +1250,66 @@ files = [ [[package]] name = "pillow" -version = "9.5.0" +version = "10.0.0" description = "Python Imaging Library (Fork)" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, - {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, - {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, - {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, - {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, - {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, - {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, - {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, - {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, - {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, - {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, - {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, - {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, - {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, - {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, - {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, - {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, - {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, - {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, - {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, - {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, - {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, - {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, - {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, - {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, - {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, - {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, - {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, - {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, - {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, - {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, + {file = "Pillow-10.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f62406a884ae75fb2f818694469519fb685cc7eaff05d3451a9ebe55c646891"}, + {file = "Pillow-10.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d5db32e2a6ccbb3d34d87c87b432959e0db29755727afb37290e10f6e8e62614"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf4392b77bdc81f36e92d3a07a5cd072f90253197f4a52a55a8cec48a12483b"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520f2a520dc040512699f20fa1c363eed506e94248d71f85412b625026f6142c"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:8c11160913e3dd06c8ffdb5f233a4f254cb449f4dfc0f8f4549eda9e542c93d1"}, + {file = "Pillow-10.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a74ba0c356aaa3bb8e3eb79606a87669e7ec6444be352870623025d75a14a2bf"}, + {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d0dae4cfd56969d23d94dc8e89fb6a217be461c69090768227beb8ed28c0a3"}, + {file = "Pillow-10.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22c10cc517668d44b211717fd9775799ccec4124b9a7f7b3635fc5386e584992"}, + {file = "Pillow-10.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:dffe31a7f47b603318c609f378ebcd57f1554a3a6a8effbc59c3c69f804296de"}, + {file = "Pillow-10.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:9fb218c8a12e51d7ead2a7c9e101a04982237d4855716af2e9499306728fb485"}, + {file = "Pillow-10.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d35e3c8d9b1268cbf5d3670285feb3528f6680420eafe35cccc686b73c1e330f"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ed64f9ca2f0a95411e88a4efbd7a29e5ce2cea36072c53dd9d26d9c76f753b3"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6eb5502f45a60a3f411c63187db83a3d3107887ad0d036c13ce836f8a36f1d"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c1fbe7621c167ecaa38ad29643d77a9ce7311583761abf7836e1510c580bf3dd"}, + {file = "Pillow-10.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cd25d2a9d2b36fcb318882481367956d2cf91329f6892fe5d385c346c0649629"}, + {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3b08d4cc24f471b2c8ca24ec060abf4bebc6b144cb89cba638c720546b1cf538"}, + {file = "Pillow-10.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737a602fbd82afd892ca746392401b634e278cb65d55c4b7a8f48e9ef8d008d"}, + {file = "Pillow-10.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3a82c40d706d9aa9734289740ce26460a11aeec2d9c79b7af87bb35f0073c12f"}, + {file = "Pillow-10.0.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:d80cf684b541685fccdd84c485b31ce73fc5c9b5d7523bf1394ce134a60c6883"}, + {file = "Pillow-10.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76de421f9c326da8f43d690110f0e79fe3ad1e54be811545d7d91898b4c8493e"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81ff539a12457809666fef6624684c008e00ff6bf455b4b89fd00a140eecd640"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce543ed15570eedbb85df19b0a1a7314a9c8141a36ce089c0a894adbfccb4568"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:685ac03cc4ed5ebc15ad5c23bc555d68a87777586d970c2c3e216619a5476223"}, + {file = "Pillow-10.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d72e2ecc68a942e8cf9739619b7f408cc7b272b279b56b2c83c6123fcfa5cdff"}, + {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d50b6aec14bc737742ca96e85d6d0a5f9bfbded018264b3b70ff9d8c33485551"}, + {file = "Pillow-10.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5"}, + {file = "Pillow-10.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:f31f9fdbfecb042d046f9d91270a0ba28368a723302786c0009ee9b9f1f60199"}, + {file = "Pillow-10.0.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:349930d6e9c685c089284b013478d6f76e3a534e36ddfa912cde493f235372f3"}, + {file = "Pillow-10.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3a684105f7c32488f7153905a4e3015a3b6c7182e106fe3c37fbb5ef3e6994c3"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4f69b3700201b80bb82c3a97d5e9254084f6dd5fb5b16fc1a7b974260f89f43"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f07ea8d2f827d7d2a49ecf1639ec02d75ffd1b88dcc5b3a61bbb37a8759ad8d"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530"}, + {file = "Pillow-10.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f88a0b92277de8e3ca715a0d79d68dc82807457dae3ab8699c758f07c20b3c51"}, + {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c7cf14a27b0d6adfaebb3ae4153f1e516df54e47e42dcc073d7b3d76111a8d86"}, + {file = "Pillow-10.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3400aae60685b06bb96f99a21e1ada7bc7a413d5f49bce739828ecd9391bb8f7"}, + {file = "Pillow-10.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbc02381779d412145331789b40cc7b11fdf449e5d94f6bc0b080db0a56ea3f0"}, + {file = "Pillow-10.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9211e7ad69d7c9401cfc0e23d49b69ca65ddd898976d660a2fa5904e3d7a9baa"}, + {file = "Pillow-10.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:faaf07ea35355b01a35cb442dd950d8f1bb5b040a7787791a535de13db15ed90"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f72a021fbb792ce98306ffb0c348b3c9cb967dce0f12a49aa4c3d3fdefa967"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f7c16705f44e0504a3a2a14197c1f0b32a95731d251777dcb060aa83022cb2d"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:76edb0a1fa2b4745fb0c99fb9fb98f8b180a1bbceb8be49b087e0b21867e77d3"}, + {file = "Pillow-10.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:368ab3dfb5f49e312231b6f27b8820c823652b7cd29cfbd34090565a015e99ba"}, + {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:608bfdee0d57cf297d32bcbb3c728dc1da0907519d1784962c5f0c68bb93e5a3"}, + {file = "Pillow-10.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5c6e3df6bdd396749bafd45314871b3d0af81ff935b2d188385e970052091017"}, + {file = "Pillow-10.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:7be600823e4c8631b74e4a0d38384c73f680e6105a7d3c6824fcf226c178c7e6"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:92be919bbc9f7d09f7ae343c38f5bb21c973d2576c1d45600fce4b74bafa7ac0"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8182b523b2289f7c415f589118228d30ac8c355baa2f3194ced084dac2dbba"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38250a349b6b390ee6047a62c086d3817ac69022c127f8a5dc058c31ccef17f3"}, + {file = "Pillow-10.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88af2003543cc40c80f6fca01411892ec52b11021b3dc22ec3bc9d5afd1c5334"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c189af0545965fa8d3b9613cfdb0cd37f9d71349e0f7750e1fd704648d475ed2"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7b031a6fc11365970e6a5686d7ba8c63e4c1cf1ea143811acbb524295eabed"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:db24668940f82321e746773a4bc617bfac06ec831e5c88b643f91f122a785684"}, + {file = "Pillow-10.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:efe8c0681042536e0d06c11f48cebe759707c9e9abf880ee213541c5b46c5bf3"}, + {file = "Pillow-10.0.0.tar.gz", hash = "sha256:9c82b5b3e043c7af0d95792d0d20ccf68f61a1fec6b3530e718b688422727396"}, ] [package.extras] @@ -1053,14 +1318,14 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa [[package]] name = "pluggy" -version = "1.0.0" +version = "1.2.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, ] [package.extras] @@ -1086,6 +1351,26 @@ tomli = ">=1.2.2" [package.extras] poetry-plugin = ["poetry (>=1.0,<2.0)"] +[[package]] +name = "portalocker" +version = "2.7.0" +description = "Wraps the portalocker recipe for easy usage" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "portalocker-2.7.0-py2.py3-none-any.whl", hash = "sha256:a07c5b4f3985c3cf4798369631fb7011adb498e2a46d8440efc75a8f29a0f983"}, + {file = "portalocker-2.7.0.tar.gz", hash = "sha256:032e81d534a88ec1736d03f780ba073f047a06c478b06e2937486f334e955c51"}, +] + +[package.dependencies] +pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} + +[package.extras] +docs = ["sphinx (>=1.7.1)"] +redis = ["redis"] +tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)"] + [[package]] name = "py" version = "1.11.0" @@ -1124,48 +1409,48 @@ files = [ [[package]] name = "pydantic" -version = "1.10.9" +version = "1.10.11" description = "Data validation and settings management using python type hints" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e692dec4a40bfb40ca530e07805b1208c1de071a18d26af4a2a0d79015b352ca"}, - {file = "pydantic-1.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c52eb595db83e189419bf337b59154bdcca642ee4b2a09e5d7797e41ace783f"}, - {file = "pydantic-1.10.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:939328fd539b8d0edf244327398a667b6b140afd3bf7e347cf9813c736211896"}, - {file = "pydantic-1.10.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b48d3d634bca23b172f47f2335c617d3fcb4b3ba18481c96b7943a4c634f5c8d"}, - {file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f0b7628fb8efe60fe66fd4adadd7ad2304014770cdc1f4934db41fe46cc8825f"}, - {file = "pydantic-1.10.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e1aa5c2410769ca28aa9a7841b80d9d9a1c5f223928ca8bec7e7c9a34d26b1d4"}, - {file = "pydantic-1.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:eec39224b2b2e861259d6f3c8b6290d4e0fbdce147adb797484a42278a1a486f"}, - {file = "pydantic-1.10.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d111a21bbbfd85c17248130deac02bbd9b5e20b303338e0dbe0faa78330e37e0"}, - {file = "pydantic-1.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e9aec8627a1a6823fc62fb96480abe3eb10168fd0d859ee3d3b395105ae19a7"}, - {file = "pydantic-1.10.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07293ab08e7b4d3c9d7de4949a0ea571f11e4557d19ea24dd3ae0c524c0c334d"}, - {file = "pydantic-1.10.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ee829b86ce984261d99ff2fd6e88f2230068d96c2a582f29583ed602ef3fc2c"}, - {file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4b466a23009ff5cdd7076eb56aca537c745ca491293cc38e72bf1e0e00de5b91"}, - {file = "pydantic-1.10.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7847ca62e581e6088d9000f3c497267868ca2fa89432714e21a4fb33a04d52e8"}, - {file = "pydantic-1.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:7845b31959468bc5b78d7b95ec52fe5be32b55d0d09983a877cca6aedc51068f"}, - {file = "pydantic-1.10.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:517a681919bf880ce1dac7e5bc0c3af1e58ba118fd774da2ffcd93c5f96eaece"}, - {file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67195274fd27780f15c4c372f4ba9a5c02dad6d50647b917b6a92bf00b3d301a"}, - {file = "pydantic-1.10.9-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2196c06484da2b3fded1ab6dbe182bdabeb09f6318b7fdc412609ee2b564c49a"}, - {file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6257bb45ad78abacda13f15bde5886efd6bf549dd71085e64b8dcf9919c38b60"}, - {file = "pydantic-1.10.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3283b574b01e8dbc982080d8287c968489d25329a463b29a90d4157de4f2baaf"}, - {file = "pydantic-1.10.9-cp37-cp37m-win_amd64.whl", hash = "sha256:5f8bbaf4013b9a50e8100333cc4e3fa2f81214033e05ac5aa44fa24a98670a29"}, - {file = "pydantic-1.10.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9cd67fb763248cbe38f0593cd8611bfe4b8ad82acb3bdf2b0898c23415a1f82"}, - {file = "pydantic-1.10.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f50e1764ce9353be67267e7fd0da08349397c7db17a562ad036aa7c8f4adfdb6"}, - {file = "pydantic-1.10.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73ef93e5e1d3c8e83f1ff2e7fdd026d9e063c7e089394869a6e2985696693766"}, - {file = "pydantic-1.10.9-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128d9453d92e6e81e881dd7e2484e08d8b164da5507f62d06ceecf84bf2e21d3"}, - {file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad428e92ab68798d9326bb3e5515bc927444a3d71a93b4a2ca02a8a5d795c572"}, - {file = "pydantic-1.10.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fab81a92f42d6d525dd47ced310b0c3e10c416bbfae5d59523e63ea22f82b31e"}, - {file = "pydantic-1.10.9-cp38-cp38-win_amd64.whl", hash = "sha256:963671eda0b6ba6926d8fc759e3e10335e1dc1b71ff2a43ed2efd6996634dafb"}, - {file = "pydantic-1.10.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:970b1bdc6243ef663ba5c7e36ac9ab1f2bfecb8ad297c9824b542d41a750b298"}, - {file = "pydantic-1.10.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7e1d5290044f620f80cf1c969c542a5468f3656de47b41aa78100c5baa2b8276"}, - {file = "pydantic-1.10.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83fcff3c7df7adff880622a98022626f4f6dbce6639a88a15a3ce0f96466cb60"}, - {file = "pydantic-1.10.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0da48717dc9495d3a8f215e0d012599db6b8092db02acac5e0d58a65248ec5bc"}, - {file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0a2aabdc73c2a5960e87c3ffebca6ccde88665616d1fd6d3db3178ef427b267a"}, - {file = "pydantic-1.10.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9863b9420d99dfa9c064042304868e8ba08e89081428a1c471858aa2af6f57c4"}, - {file = "pydantic-1.10.9-cp39-cp39-win_amd64.whl", hash = "sha256:e7c9900b43ac14110efa977be3da28931ffc74c27e96ee89fbcaaf0b0fe338e1"}, - {file = "pydantic-1.10.9-py3-none-any.whl", hash = "sha256:6cafde02f6699ce4ff643417d1a9223716ec25e228ddc3b436fe7e2d25a1f305"}, - {file = "pydantic-1.10.9.tar.gz", hash = "sha256:95c70da2cd3b6ddf3b9645ecaa8d98f3d80c606624b6d245558d202cd23ea3be"}, + {file = "pydantic-1.10.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ff44c5e89315b15ff1f7fdaf9853770b810936d6b01a7bcecaa227d2f8fe444f"}, + {file = "pydantic-1.10.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c098d4ab5e2d5b3984d3cb2527e2d6099d3de85630c8934efcfdc348a9760e"}, + {file = "pydantic-1.10.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16928fdc9cb273c6af00d9d5045434c39afba5f42325fb990add2c241402d151"}, + {file = "pydantic-1.10.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0588788a9a85f3e5e9ebca14211a496409cb3deca5b6971ff37c556d581854e7"}, + {file = "pydantic-1.10.11-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e9baf78b31da2dc3d3f346ef18e58ec5f12f5aaa17ac517e2ffd026a92a87588"}, + {file = "pydantic-1.10.11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:373c0840f5c2b5b1ccadd9286782852b901055998136287828731868027a724f"}, + {file = "pydantic-1.10.11-cp310-cp310-win_amd64.whl", hash = "sha256:c3339a46bbe6013ef7bdd2844679bfe500347ac5742cd4019a88312aa58a9847"}, + {file = "pydantic-1.10.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08a6c32e1c3809fbc49debb96bf833164f3438b3696abf0fbeceb417d123e6eb"}, + {file = "pydantic-1.10.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a451ccab49971af043ec4e0d207cbc8cbe53dbf148ef9f19599024076fe9c25b"}, + {file = "pydantic-1.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b02d24f7b2b365fed586ed73582c20f353a4c50e4be9ba2c57ab96f8091ddae"}, + {file = "pydantic-1.10.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f34739a89260dfa420aa3cbd069fbcc794b25bbe5c0a214f8fb29e363484b66"}, + {file = "pydantic-1.10.11-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e297897eb4bebde985f72a46a7552a7556a3dd11e7f76acda0c1093e3dbcf216"}, + {file = "pydantic-1.10.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d185819a7a059550ecb85d5134e7d40f2565f3dd94cfd870132c5f91a89cf58c"}, + {file = "pydantic-1.10.11-cp311-cp311-win_amd64.whl", hash = "sha256:4400015f15c9b464c9db2d5d951b6a780102cfa5870f2c036d37c23b56f7fc1b"}, + {file = "pydantic-1.10.11-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2417de68290434461a266271fc57274a138510dca19982336639484c73a07af6"}, + {file = "pydantic-1.10.11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:331c031ba1554b974c98679bd0780d89670d6fd6f53f5d70b10bdc9addee1713"}, + {file = "pydantic-1.10.11-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8268a735a14c308923e8958363e3a3404f6834bb98c11f5ab43251a4e410170c"}, + {file = "pydantic-1.10.11-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:44e51ba599c3ef227e168424e220cd3e544288c57829520dc90ea9cb190c3248"}, + {file = "pydantic-1.10.11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d7781f1d13b19700b7949c5a639c764a077cbbdd4322ed505b449d3ca8edcb36"}, + {file = "pydantic-1.10.11-cp37-cp37m-win_amd64.whl", hash = "sha256:7522a7666157aa22b812ce14c827574ddccc94f361237ca6ea8bb0d5c38f1629"}, + {file = "pydantic-1.10.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc64eab9b19cd794a380179ac0e6752335e9555d214cfcb755820333c0784cb3"}, + {file = "pydantic-1.10.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8dc77064471780262b6a68fe67e013298d130414d5aaf9b562c33987dbd2cf4f"}, + {file = "pydantic-1.10.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe429898f2c9dd209bd0632a606bddc06f8bce081bbd03d1c775a45886e2c1cb"}, + {file = "pydantic-1.10.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:192c608ad002a748e4a0bed2ddbcd98f9b56df50a7c24d9a931a8c5dd053bd3d"}, + {file = "pydantic-1.10.11-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ef55392ec4bb5721f4ded1096241e4b7151ba6d50a50a80a2526c854f42e6a2f"}, + {file = "pydantic-1.10.11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41e0bb6efe86281623abbeeb0be64eab740c865388ee934cd3e6a358784aca6e"}, + {file = "pydantic-1.10.11-cp38-cp38-win_amd64.whl", hash = "sha256:265a60da42f9f27e0b1014eab8acd3e53bd0bad5c5b4884e98a55f8f596b2c19"}, + {file = "pydantic-1.10.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:469adf96c8e2c2bbfa655fc7735a2a82f4c543d9fee97bd113a7fb509bf5e622"}, + {file = "pydantic-1.10.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6cbfbd010b14c8a905a7b10f9fe090068d1744d46f9e0c021db28daeb8b6de1"}, + {file = "pydantic-1.10.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abade85268cc92dff86d6effcd917893130f0ff516f3d637f50dadc22ae93999"}, + {file = "pydantic-1.10.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9738b0f2e6c70f44ee0de53f2089d6002b10c33264abee07bdb5c7f03038303"}, + {file = "pydantic-1.10.11-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:787cf23e5a0cde753f2eabac1b2e73ae3844eb873fd1f5bdbff3048d8dbb7604"}, + {file = "pydantic-1.10.11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:174899023337b9fc685ac8adaa7b047050616136ccd30e9070627c1aaab53a13"}, + {file = "pydantic-1.10.11-cp39-cp39-win_amd64.whl", hash = "sha256:1954f8778489a04b245a1e7b8b22a9d3ea8ef49337285693cf6959e4b757535e"}, + {file = "pydantic-1.10.11-py3-none-any.whl", hash = "sha256:008c5e266c8aada206d0627a011504e14268a62091450210eda7c07fabe6963e"}, + {file = "pydantic-1.10.11.tar.gz", hash = "sha256:f66d479cf7eb331372c470614be6511eae96f1f120344c25f3f9bb59fb1b5528"}, ] [package.dependencies] @@ -1175,6 +1460,21 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pydot" +version = "1.4.2" +description = "Python interface to Graphviz's Dot" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pydot-1.4.2-py2.py3-none-any.whl", hash = "sha256:66c98190c65b8d2e2382a441b4c0edfdb4f4c025ef9cb9874de478fb0793a451"}, + {file = "pydot-1.4.2.tar.gz", hash = "sha256:248081a39bcb56784deb018977e428605c1c758f10897a339fce1dd728ff007d"}, +] + +[package.dependencies] +pyparsing = ">=2.1.4" + [[package]] name = "pyexasol" version = "0.25.2" @@ -1200,6 +1500,33 @@ pandas = ["pandas"] rapidjson = ["python-rapidjson"] ujson = ["ujson"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + [[package]] name = "pyopenssl" version = "23.2.0" @@ -1234,16 +1561,28 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyreadline3" +version = "3.4.1" +description = "A python implementation of GNU readline." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, + {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, +] + [[package]] name = "pytest" -version = "7.3.2" +version = "7.4.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.2-py3-none-any.whl", hash = "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295"}, - {file = "pytest-7.3.2.tar.gz", hash = "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b"}, + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, ] [package.dependencies] @@ -1307,6 +1646,27 @@ files = [ [package.dependencies] pytest = ">=3.6" +[[package]] +name = "python-daemon" +version = "3.0.1" +description = "Library to implement a well-behaved Unix daemon process." +category = "dev" +optional = false +python-versions = ">=3" +files = [ + {file = "python-daemon-3.0.1.tar.gz", hash = "sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5"}, + {file = "python_daemon-3.0.1-py3-none-any.whl", hash = "sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341"}, +] + +[package.dependencies] +docutils = "*" +lockfile = ">=0.10" +setuptools = ">=62.4.0" + +[package.extras] +devel = ["coverage", "docutils", "isort", "testscenarios (>=0.4)", "testtools", "twine"] +test = ["coverage", "docutils", "testscenarios (>=0.4)", "testtools"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1334,6 +1694,30 @@ files = [ {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, ] +[[package]] +name = "pywin32" +version = "306" +description = "Python for Window Extensions" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + [[package]] name = "pyzmq" version = "24.0.1" @@ -1461,46 +1845,46 @@ pyasn1 = ">=0.1.3" [[package]] name = "scikit-learn" -version = "1.2.2" +version = "1.3.0" description = "A set of python modules for machine learning and data mining" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "scikit-learn-1.2.2.tar.gz", hash = "sha256:8429aea30ec24e7a8c7ed8a3fa6213adf3814a6efbea09e16e0a0c71e1a1a3d7"}, - {file = "scikit_learn-1.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99cc01184e347de485bf253d19fcb3b1a3fb0ee4cea5ee3c43ec0cc429b6d29f"}, - {file = "scikit_learn-1.2.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e6e574db9914afcb4e11ade84fab084536a895ca60aadea3041e85b8ac963edb"}, - {file = "scikit_learn-1.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fe83b676f407f00afa388dd1fdd49e5c6612e551ed84f3b1b182858f09e987d"}, - {file = "scikit_learn-1.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2642baa0ad1e8f8188917423dd73994bf25429f8893ddbe115be3ca3183584"}, - {file = "scikit_learn-1.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ad66c3848c0a1ec13464b2a95d0a484fd5b02ce74268eaa7e0c697b904f31d6c"}, - {file = "scikit_learn-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfeaf8be72117eb61a164ea6fc8afb6dfe08c6f90365bde2dc16456e4bc8e45f"}, - {file = "scikit_learn-1.2.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:fe0aa1a7029ed3e1dcbf4a5bc675aa3b1bc468d9012ecf6c6f081251ca47f590"}, - {file = "scikit_learn-1.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:065e9673e24e0dc5113e2dd2b4ca30c9d8aa2fa90f4c0597241c93b63130d233"}, - {file = "scikit_learn-1.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf036ea7ef66115e0d49655f16febfa547886deba20149555a41d28f56fd6d3c"}, - {file = "scikit_learn-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8b0670d4224a3c2d596fd572fb4fa673b2a0ccfb07152688ebd2ea0b8c61025c"}, - {file = "scikit_learn-1.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9c710ff9f9936ba8a3b74a455ccf0dcf59b230caa1e9ba0223773c490cab1e51"}, - {file = "scikit_learn-1.2.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:2dd3ffd3950e3d6c0c0ef9033a9b9b32d910c61bd06cb8206303fb4514b88a49"}, - {file = "scikit_learn-1.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44b47a305190c28dd8dd73fc9445f802b6ea716669cfc22ab1eb97b335d238b1"}, - {file = "scikit_learn-1.2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:953236889928d104c2ef14027539f5f2609a47ebf716b8cbe4437e85dce42744"}, - {file = "scikit_learn-1.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:7f69313884e8eb311460cc2f28676d5e400bd929841a2c8eb8742ae78ebf7c20"}, - {file = "scikit_learn-1.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8156db41e1c39c69aa2d8599ab7577af53e9e5e7a57b0504e116cc73c39138dd"}, - {file = "scikit_learn-1.2.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fe175ee1dab589d2e1033657c5b6bec92a8a3b69103e3dd361b58014729975c3"}, - {file = "scikit_learn-1.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d5312d9674bed14f73773d2acf15a3272639b981e60b72c9b190a0cffed5bad"}, - {file = "scikit_learn-1.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea061bf0283bf9a9f36ea3c5d3231ba2176221bbd430abd2603b1c3b2ed85c89"}, - {file = "scikit_learn-1.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:6477eed40dbce190f9f9e9d0d37e020815825b300121307942ec2110302b66a3"}, + {file = "scikit-learn-1.3.0.tar.gz", hash = "sha256:8be549886f5eda46436b6e555b0e4873b4f10aa21c07df45c4bc1735afbccd7a"}, + {file = "scikit_learn-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981287869e576d42c682cf7ca96af0c6ac544ed9316328fd0d9292795c742cf5"}, + {file = "scikit_learn-1.3.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:436aaaae2c916ad16631142488e4c82f4296af2404f480e031d866863425d2a2"}, + {file = "scikit_learn-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7e28d8fa47a0b30ae1bd7a079519dd852764e31708a7804da6cb6f8b36e3630"}, + {file = "scikit_learn-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80c08834a473d08a204d966982a62e11c976228d306a2648c575e3ead12111"}, + {file = "scikit_learn-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:552fd1b6ee22900cf1780d7386a554bb96949e9a359999177cf30211e6b20df6"}, + {file = "scikit_learn-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79970a6d759eb00a62266a31e2637d07d2d28446fca8079cf9afa7c07b0427f8"}, + {file = "scikit_learn-1.3.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:850a00b559e636b23901aabbe79b73dc604b4e4248ba9e2d6e72f95063765603"}, + {file = "scikit_learn-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee04835fb016e8062ee9fe9074aef9b82e430504e420bff51e3e5fffe72750ca"}, + {file = "scikit_learn-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d953531f5d9f00c90c34fa3b7d7cfb43ecff4c605dac9e4255a20b114a27369"}, + {file = "scikit_learn-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:151ac2bf65ccf363664a689b8beafc9e6aae36263db114b4ca06fbbbf827444a"}, + {file = "scikit_learn-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a885a9edc9c0a341cab27ec4f8a6c58b35f3d449c9d2503a6fd23e06bbd4f6a"}, + {file = "scikit_learn-1.3.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:9877af9c6d1b15486e18a94101b742e9d0d2f343d35a634e337411ddb57783f3"}, + {file = "scikit_learn-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c470f53cea065ff3d588050955c492793bb50c19a92923490d18fcb637f6383a"}, + {file = "scikit_learn-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6e2d7389542eae01077a1ee0318c4fec20c66c957f45c7aac0c6eb0fe3c612"}, + {file = "scikit_learn-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:3a11936adbc379a6061ea32fa03338d4ca7248d86dd507c81e13af428a5bc1db"}, + {file = "scikit_learn-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:998d38fcec96584deee1e79cd127469b3ad6fefd1ea6c2dfc54e8db367eb396b"}, + {file = "scikit_learn-1.3.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ded35e810438a527e17623ac6deae3b360134345b7c598175ab7741720d7ffa7"}, + {file = "scikit_learn-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e8102d5036e28d08ab47166b48c8d5e5810704daecf3a476a4282d562be9a28"}, + {file = "scikit_learn-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7617164951c422747e7c32be4afa15d75ad8044f42e7d70d3e2e0429a50e6718"}, + {file = "scikit_learn-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:1d54fb9e6038284548072df22fd34777e434153f7ffac72c8596f2d6987110dd"}, ] [package.dependencies] joblib = ">=1.1.1" numpy = ">=1.17.3" -scipy = ">=1.3.2" +scipy = ">=1.5.0" threadpoolctl = ">=2.0.0" [package.extras] benchmark = ["matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "pandas (>=1.0.5)"] -docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.10.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=4.0.1)", "sphinx-gallery (>=0.7.0)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] -examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.10.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] -tests = ["black (>=22.3.0)", "flake8 (>=3.8.2)", "matplotlib (>=3.1.3)", "mypy (>=0.961)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=5.3.1)", "pytest-cov (>=2.9.0)", "scikit-image (>=0.16.2)"] +docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)", "sphinx (>=6.0.0)", "sphinx-copybutton (>=0.5.2)", "sphinx-gallery (>=0.10.1)", "sphinx-prompt (>=1.3.0)", "sphinxext-opengraph (>=0.4.2)"] +examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] +tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"] [[package]] name = "scipy" @@ -1665,6 +2049,18 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "smmap" +version = "5.0.0" +description = "A pure Python implementation of a sliding window memory map manager" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1677,6 +2073,18 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "stopwatch-py" +version = "2.0.1" +description = "A simple stopwatch for python" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "stopwatch.py-2.0.1-py3-none-any.whl", hash = "sha256:5802a0178d766120c11dd5df8ae838e9beccb8c88329dbd5f0f7ac4b7fed9107"}, + {file = "stopwatch.py-2.0.1.tar.gz", hash = "sha256:8cc94ba0f6469d434eabd8b227166e595fd42350e7f66dbf1a1a80697f60cc79"}, +] + [[package]] name = "structlog" version = "22.3.0" @@ -1734,6 +2142,27 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tornado" +version = "6.3.2" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "dev" +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.3.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829"}, + {file = "tornado-6.3.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c"}, + {file = "tornado-6.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f"}, + {file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4"}, + {file = "tornado-6.3.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe"}, + {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d"}, + {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0"}, + {file = "tornado-6.3.2-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411"}, + {file = "tornado-6.3.2-cp38-abi3-win32.whl", hash = "sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2"}, + {file = "tornado-6.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf"}, + {file = "tornado-6.3.2.tar.gz", hash = "sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba"}, +] + [[package]] name = "typeguard" version = "2.13.3" @@ -1752,14 +2181,14 @@ test = ["mypy", "pytest", "typing-extensions"] [[package]] name = "typing-extensions" -version = "4.6.3" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"}, - {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [[package]] @@ -1782,14 +2211,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "websocket-client" -version = "1.6.0" +version = "1.6.1" description = "WebSocket client for Python with low level API options" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "websocket-client-1.6.0.tar.gz", hash = "sha256:e84c7eafc66aade6d1967a51dfd219aabdf81d15b9705196e11fd81f48666b78"}, - {file = "websocket_client-1.6.0-py3-none-any.whl", hash = "sha256:72d7802608745b0a212f79b478642473bd825777d8637b6c8c421bf167790d4f"}, + {file = "websocket-client-1.6.1.tar.gz", hash = "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd"}, + {file = "websocket_client-1.6.1-py3-none-any.whl", hash = "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"}, ] [package.extras] @@ -1816,4 +2245,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "4daebba8cce2bfcd59cb5145b79d0e2b67f42a6d801adcac8a67ad97f59e6d44" +content-hash = "9bddea4f4be7886cc47ac856cd333dce46192700135b58e115f61282313774c1" diff --git a/pyproject.toml b/pyproject.toml index 675451c1..4043e6c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ poethepoet = "^0.13.0" pytest-assume = "^2.4.3" exasol-udf-mock-python = { git = "https://github.com/exasol/udf-mock-python.git", branch = "main" } pytest-cov = "^3.0.0" +exasol-integration-test-docker-environment = "1.7.1" [tool.poetry.group.dev.dependencies] pytest-repeat = "^0.9.1" diff --git a/scripts/start_integration_test_environment.sh b/scripts/start_integration_test_environment.sh index 7f37cfbb..aeb7f044 100755 --- a/scripts/start_integration_test_environment.sh +++ b/scripts/start_integration_test_environment.sh @@ -2,19 +2,4 @@ set -euo pipefail -GIT_REF=main -CHECKOUT_PATH=/tmp/integration-test-docker-environment - -if [ -d "$CHECKOUT_PATH" ] -then - cd "$CHECKOUT_PATH" - git checkout "$GIT_REF" - git pull -else - git clone https://github.com/exasol/integration-test-docker-environment.git "$CHECKOUT_PATH" - cd "$CHECKOUT_PATH" - git checkout "$GIT_REF" -fi - - -./start-test-env spawn-test-environment --environment-name test --database-port-forward 9563 --bucketfs-port-forward 6666 --db-mem-size 4GB --nameserver 8.8.8.8 +./scripts/run_in_dev_env.sh poetry run itde spawn-test-environment --environment-name test --database-port-forward 9563 --bucketfs-port-forward 6666 --db-mem-size 4GB --nameserver 8.8.8.8 diff --git a/tests/udf_communication/peer_communication/analyze_log.py b/tests/udf_communication/peer_communication/analyze_log.py new file mode 100644 index 00000000..b8a294d1 --- /dev/null +++ b/tests/udf_communication/peer_communication/analyze_log.py @@ -0,0 +1,117 @@ +import json +from collections import defaultdict, Counter +from pathlib import Path +from typing import Dict, List, Callable + + +def is_log_sequence_ok(lines: List[Dict[str, str]], line_predicate: Callable[[Dict[str, str]], bool]): + result = False + for line in lines: + if line_predicate(line): + result = True + return result + + +def is_peer_ready(line: Dict[str, str]): + return line["module"] == "peer_is_ready_sender" and line["event"] == "send" + + +def is_connection_acknowledged(line: Dict[str, str]): + return line["module"] == "background_peer_state" and line["event"] == "received_acknowledge_connection" + + +def is_connection_synchronized(line: Dict[str, str]): + return line["module"] == "background_peer_state" and line["event"] == "received_synchronize_connection" + + +def analyze_source_target_interaction(): + print("analyze_source_target_interaction") + root = Path(__file__).parent + with open(root / "test_add_peer.log") as f: + group_source_target_map = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) + collect_source_target_interaction(f, group_source_target_map) + print_source_target_interaction(group_source_target_map) + + +def print_source_target_interaction(group_source_target_map): + predicates = { + "is_peer_ready": is_peer_ready, + "is_connection_acknowledged": is_connection_acknowledged, + "is_connection_synchronized": is_connection_synchronized + } + for group, sources in group_source_target_map.items(): + ok = Counter() + not_ok = Counter() + for source, targets in sources.items(): + for target, lines in targets.items(): + for predicate_name, predicate in predicates.items(): + if not is_log_sequence_ok(lines, predicate): + print(f"========== {predicate_name}-{group}-{source}-{target} ============") + not_ok.update((predicate_name,)) + else: + ok.update((predicate_name,)) + for predicate_name in predicates.keys(): + print(f"{group} {predicate_name} ok {ok[predicate_name]} not_ok {not_ok[predicate_name]}") + + +def collect_source_target_interaction(f, group_source_target_map): + for line in iter(f.readline, ""): + json_line = json.loads(line) + if ("peer" in json_line + and "my_connection_info" in json_line + and "event" in json_line + and "module" in json_line + and json_line["event"] != "send_if_necessary" + ): + group = json_line["my_connection_info"]["group_identifier"] + source = json_line["my_connection_info"]["name"] + target = json_line["peer"]["connection_info"]["name"] + group_source_target_map[group][source][target].append({ + "event": json_line["event"], + "module": json_line["module"], + "timestamp": json_line["timestamp"], + }) + + +def collect_close(f, group_source_map): + for line in iter(f.readline, ""): + json_line = json.loads(line) + if ("name" in json_line + and "group_identifier" in json_line + and "event" in json_line + and json_line["event"].startswith("after") + ): + group = json_line["group_identifier"] + source = json_line["name"] + group_source_map[group][source].append({ + "timestamp": json_line["timestamp"], + "event": json_line["event"] + }) + + +def print_close(group_source_map): + for group, sources in group_source_map.items(): + for source, lines in sources.items(): + # There are two steps for closing a test. + # First, closing the PeerCommunicator. + # Second, closing the zmq context. + # If we have more or less, something is off. + if len(lines) != 2: + print(f"============== {group}-{source} ===============") + for line in lines: + print(line) + print(f"{group} after ... {len(sources)}") + + +def analyze_close(): + print("analyze_close") + root = Path(__file__).parent + with open(root / "test_add_peer.log") as f: + group_source_map = defaultdict(lambda: defaultdict(list)) + collect_close(f, group_source_map) + print_close(group_source_map) + + +if __name__ == "__main__": + analyze_source_target_interaction() + analyze_close() diff --git a/tests/udf_communication/peer_communication/conditional_method_dropper.py b/tests/udf_communication/peer_communication/conditional_method_dropper.py new file mode 100644 index 00000000..43eb4c12 --- /dev/null +++ b/tests/udf_communication/peer_communication/conditional_method_dropper.py @@ -0,0 +1,12 @@ +from structlog import DropEvent + + +class ConditionalMethodDropper: + def __init__(self, method_name): + self._method_name = method_name + + def __call__(self, logger, method_name, event_dict): + if method_name == self._method_name: + raise DropEvent + + return event_dict diff --git a/tests/udf_communication/peer_communication/mock_cast.py b/tests/udf_communication/peer_communication/mock_cast.py new file mode 100644 index 00000000..3a3866ea --- /dev/null +++ b/tests/udf_communication/peer_communication/mock_cast.py @@ -0,0 +1,6 @@ +from typing import Any, cast +from unittest.mock import Mock + + +def mock_cast(obj: Any) -> Mock: + return cast(Mock, obj) diff --git a/tests/udf_communication/peer_communication/test_abort_timeout_sender.py b/tests/udf_communication/peer_communication/test_abort_timeout_sender.py new file mode 100644 index 00000000..6799828a --- /dev/null +++ b/tests/udf_communication/peer_communication/test_abort_timeout_sender.py @@ -0,0 +1,168 @@ +import dataclasses +from typing import Union, cast, Any +from unittest.mock import MagicMock, Mock, create_autospec, call + +from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo +from exasol_advanced_analytics_framework.udf_communication.ip_address import IPAddress, Port +from exasol_advanced_analytics_framework.udf_communication.messages import TimeoutMessage +from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.abort_timeout_sender import \ + AbortTimeoutSender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.timer import Timer +from exasol_advanced_analytics_framework.udf_communication.serialization import serialize_message +from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import Socket + + +def mock_cast(obj: Any) -> Mock: + return cast(Mock, obj) + + +@dataclasses.dataclass() +class TestSetup: + timer_mock: Union[MagicMock, Timer] + out_control_socket_mock: Union[MagicMock, Socket] + abort_timeout_sender: AbortTimeoutSender = None + + def reset_mock(self): + self.out_control_socket_mock.reset_mock() + self.timer_mock.reset_mock() + + +def create_test_setup(): + peer = Peer( + connection_info=ConnectionInfo( + name="t2", + ipaddress=IPAddress(ip_address="127.0.0.1"), + port=Port(port=12), + group_identifier="g" + )) + my_connection_info = ConnectionInfo( + name="t1", + ipaddress=IPAddress(ip_address="127.0.0.1"), + port=Port(port=11), + group_identifier="g" + ) + timer_mock = create_autospec(Timer) + out_control_socket_mock = create_autospec(Socket) + abort_timeout_sender = AbortTimeoutSender( + peer=peer, + my_connection_info=my_connection_info, + out_control_socket=out_control_socket_mock, + timer=timer_mock + ) + return TestSetup( + timer_mock=timer_mock, + out_control_socket_mock=out_control_socket_mock, + abort_timeout_sender=abort_timeout_sender + ) + + +def test_init(): + test_setup = create_test_setup() + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [] + ) + + +def test_send_if_necessary_after_init_and_is_time_false(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = False + test_setup.reset_mock() + + test_setup.abort_timeout_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [ + call.is_time() + ] + ) + + +def test_send_if_necessary_after_init_and_is_time_true(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.reset_mock() + + test_setup.abort_timeout_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == + [ + call.send(serialize_message(TimeoutMessage())) + ] + and test_setup.timer_mock.mock_calls == [ + call.is_time() + ] + ) + + +def test_send_if_necessary_twice_and_is_time_false(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = False + test_setup.abort_timeout_sender.send_if_necessary() + test_setup.reset_mock() + + test_setup.abort_timeout_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [ + call.is_time() + ] + ) + + +def test_send_if_necessary_twice_and_is_time_true(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.abort_timeout_sender.send_if_necessary() + test_setup.reset_mock() + + test_setup.abort_timeout_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [ + call.is_time() + ] + ) + + +def test_send_if_necessary_after_stop_and_is_time_false(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = False + test_setup.abort_timeout_sender.stop() + test_setup.reset_mock() + + test_setup.abort_timeout_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [call.is_time()] + ) + + +def test_send_if_necessary_after_stop_and_is_time_true(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.abort_timeout_sender.stop() + test_setup.reset_mock() + + test_setup.abort_timeout_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [call.is_time()] + ) + + +def test_reset_timer(): + test_setup = create_test_setup() + print(test_setup.timer_mock.mock_calls) + test_setup.abort_timeout_sender.reset_timer() + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [call.reset_timer()] + ) diff --git a/tests/udf_communication/peer_communication/test_add_peer.py b/tests/udf_communication/peer_communication/test_add_peer.py index 9d67514c..3851705e 100644 --- a/tests/udf_communication/peer_communication/test_add_peer.py +++ b/tests/udf_communication/peer_communication/test_add_peer.py @@ -1,3 +1,4 @@ +import sys import time import traceback from pathlib import Path @@ -8,6 +9,7 @@ import zmq from numpy.random import RandomState from structlog import WriteLoggerFactory +from structlog.tracebacks import ExceptionDictTransformer from structlog.types import FilteringBoundLogger from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo @@ -18,6 +20,7 @@ from exasol_advanced_analytics_framework.udf_communication.socket_factory.fault_injection import \ FaultInjectionSocketFactory from exasol_advanced_analytics_framework.udf_communication.socket_factory.zmq_wrapper import ZMQSocketFactory +from tests.udf_communication.peer_communication.conditional_method_dropper import ConditionalMethodDropper from tests.udf_communication.peer_communication.utils import TestProcess, BidirectionalQueue, assert_processes_finish structlog.configure( @@ -25,15 +28,16 @@ logger_factory=WriteLoggerFactory(file=Path(__file__).with_suffix(".log").open("wt")), processors=[ structlog.contextvars.merge_contextvars, + ConditionalMethodDropper(method_name="debug"), structlog.processors.add_log_level, - structlog.processors.StackInfoRenderer(), - structlog.dev.set_exc_info, structlog.processors.TimeStamper(), + structlog.processors.ExceptionRenderer(exception_formatter=ExceptionDictTransformer(locals_max_string=320)), + structlog.processors.CallsiteParameterAdder(), structlog.processors.JSONRenderer() ] ) -LOGGER: FilteringBoundLogger = structlog.get_logger().bind(module_name=__name__) +LOGGER: FilteringBoundLogger = structlog.get_logger() def run(name: str, group_identifier: str, number_of_instances: int, queue: BidirectionalQueue, seed: int): @@ -42,7 +46,7 @@ def run(name: str, group_identifier: str, number_of_instances: int, queue: Bidir listen_ip = IPAddress(ip_address=f"127.1.0.1") context = zmq.Context() socket_factory = ZMQSocketFactory(context) - socket_factory = FaultInjectionSocketFactory(socket_factory, 0.0, RandomState(seed)) + socket_factory = FaultInjectionSocketFactory(socket_factory, 0.01, RandomState(seed)) com = PeerCommunicator( name=name, number_of_peers=number_of_instances, @@ -55,30 +59,57 @@ def run(name: str, group_identifier: str, number_of_instances: int, queue: Bidir for index, connection_info in peer_connection_infos.items(): com.register_peer(connection_info) peers = com.peers(timeout_in_milliseconds=None) - logger.info("peers", peers=peers) + logger.info("peers", number_of_peers=len(peers)) queue.put(peers) finally: com.close() + logger.info("after close") + context.destroy(linger=0) + logger.info("after destroy") + for frame in sys._current_frames().values(): + stacktrace = traceback.format_stack(frame) + logger.info("Frame", stacktrace=stacktrace) except Exception as e: - logger.exception("Exception during test", stacktrace=traceback.format_exc()) + queue.put([]) + logger.exception("Exception during test") @pytest.mark.parametrize("number_of_instances, repetitions", [(2, 1000), (10, 100)]) def test_reliability(number_of_instances: int, repetitions: int): + run_test_with_repetitions(number_of_instances, repetitions) + + +REPETITIONS_FOR_FUNCTIONALITY = 2 + + +def test_functionality_2(): + run_test_with_repetitions(2, REPETITIONS_FOR_FUNCTIONALITY) + + +def test_functionality_10(): + run_test_with_repetitions(10, REPETITIONS_FOR_FUNCTIONALITY) + + +def test_functionality_25(): + run_test_with_repetitions(25, REPETITIONS_FOR_FUNCTIONALITY) + + +def run_test_with_repetitions(number_of_instances: int, repetitions: int): for i in range(repetitions): + LOGGER.info(f"Start iteration", + iteration=i + 1, + repetitions=repetitions, + number_of_instances=number_of_instances) + start_time = time.monotonic() group = f"{time.monotonic_ns()}" expected_peers_of_threads, peers_of_threads = run_test(group, number_of_instances, seed=i) assert expected_peers_of_threads == peers_of_threads - - -def test_functionality(): - group = f"{time.monotonic_ns()}" - logger = LOGGER.bind(group=group, location="test") - logger.info("start") - number_of_instances = 2 - expected_peers_of_threads, peers_of_threads = run_test(group, number_of_instances, 0) - assert expected_peers_of_threads == peers_of_threads - logger.info("success") + end_time = time.monotonic() + LOGGER.info(f"Finish iteration", + iteration=i + 1, + repetitions=repetitions, + number_of_instances=number_of_instances, + duration=end_time - start_time) def run_test(group: str, number_of_instances: int, seed: int): @@ -90,7 +121,7 @@ def run_test(group: str, number_of_instances: int, seed: int): connection_infos[i] = processes[i].get() for i in range(number_of_instances): t = processes[i].put(connection_infos) - assert_processes_finish(processes, timeout_in_seconds=240) + assert_processes_finish(processes, timeout_in_seconds=180) peers_of_threads: Dict[int, List[ConnectionInfo]] = {} for i in range(number_of_instances): peers_of_threads[i] = processes[i].get() diff --git a/tests/udf_communication/peer_communication/test_background_peer_state.py b/tests/udf_communication/peer_communication/test_background_peer_state.py new file mode 100644 index 00000000..ac5bed5a --- /dev/null +++ b/tests/udf_communication/peer_communication/test_background_peer_state.py @@ -0,0 +1,149 @@ +import dataclasses +from typing import Union, cast, Any +from unittest.mock import MagicMock, Mock, create_autospec, call + +from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import Socket, \ + SocketFactory, SocketType + +from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo +from exasol_advanced_analytics_framework.udf_communication.ip_address import IPAddress, Port +from exasol_advanced_analytics_framework.udf_communication.messages import AcknowledgeConnectionMessage, Message +from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.abort_timeout_sender import \ + AbortTimeoutSender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.background_peer_state import \ + BackgroundPeerState +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.peer_is_ready_sender import \ + PeerIsReadySender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.sender import Sender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.synchronize_connection_sender import \ + SynchronizeConnectionSender + + +def mock_cast(obj: Any) -> Mock: + return cast(Mock, obj) + + +@dataclasses.dataclass() +class TestSetup: + peer: Peer + my_connection_info: ConnectionInfo + socket_factory_mock: Union[MagicMock, SocketFactory] + receive_socket_mock: Union[MagicMock, Socket] + sender_mock: Union[MagicMock, Sender] + abort_timeout_sender_mock: Union[MagicMock, AbortTimeoutSender] + peer_is_ready_sender_mock: Union[MagicMock, PeerIsReadySender] + synchronize_connection_sender_mock: Union[MagicMock, SynchronizeConnectionSender] + background_peer_state: BackgroundPeerState + + def reset_mocks(self): + mocks = ( + self.abort_timeout_sender_mock, + self.synchronize_connection_sender_mock, + self.peer_is_ready_sender_mock, + self.sender_mock, + self.receive_socket_mock, + self.socket_factory_mock, + ) + for mock in mocks: + mock.reset_mock() + + +def create_test_setup() -> TestSetup: + peer = Peer( + connection_info=ConnectionInfo( + name="t1", + ipaddress=IPAddress(ip_address="127.0.0.1"), + port=Port(port=11), + group_identifier="g" + )) + my_connection_info = ConnectionInfo( + name="t0", + ipaddress=IPAddress(ip_address="127.0.0.1"), + port=Port(port=10), + group_identifier="g" + ) + receive_socket_mock = create_autospec(Socket) + socket_factory_mock: Union[MagicMock, SocketFactory] = create_autospec(SocketFactory) + mock_cast(socket_factory_mock.create_socket).side_effect = [receive_socket_mock] + sender_mock = create_autospec(Sender) + abort_timeout_sender_mock = create_autospec(AbortTimeoutSender) + peer_is_ready_sender_mock = create_autospec(PeerIsReadySender) + synchronize_connection_sender_mock = create_autospec(SynchronizeConnectionSender) + background_peer_state = BackgroundPeerState( + my_connection_info=my_connection_info, + peer=peer, + socket_factory=socket_factory_mock, + sender=sender_mock, + abort_timeout_sender=abort_timeout_sender_mock, + peer_is_ready_sender=peer_is_ready_sender_mock, + synchronize_connection_sender=synchronize_connection_sender_mock + ) + return TestSetup( + peer=peer, + my_connection_info=my_connection_info, + socket_factory_mock=socket_factory_mock, + sender_mock=sender_mock, + abort_timeout_sender_mock=abort_timeout_sender_mock, + peer_is_ready_sender_mock=peer_is_ready_sender_mock, + synchronize_connection_sender_mock=synchronize_connection_sender_mock, + background_peer_state=background_peer_state, + receive_socket_mock=receive_socket_mock + ) + + +def test_init(): + test_setup = create_test_setup() + assert ( + test_setup.synchronize_connection_sender_mock.mock_calls == [call.send_if_necessary(force=True)] + and test_setup.peer_is_ready_sender_mock.mock_calls == [] + and test_setup.abort_timeout_sender_mock.mock_calls == [] + and test_setup.sender_mock.mock_calls == [] + and mock_cast(test_setup.socket_factory_mock.create_socket).mock_calls == [call(SocketType.PAIR)] + and test_setup.receive_socket_mock.mock_calls == [ + call.bind('inproc://peer/g/127.0.0.1/11') + ] + ) + + +def test_resend(): + test_setup = create_test_setup() + test_setup.reset_mocks() + test_setup.background_peer_state.resend_if_necessary() + assert ( + test_setup.synchronize_connection_sender_mock.mock_calls == [call.send_if_necessary()] + and test_setup.peer_is_ready_sender_mock.mock_calls == [call.send_if_necessary()] + and test_setup.abort_timeout_sender_mock.mock_calls == [call.send_if_necessary()] + and test_setup.sender_mock.mock_calls == [] + and mock_cast(test_setup.socket_factory_mock.create_socket).mock_calls == [] + and test_setup.receive_socket_mock.mock_calls == [] + ) + + +def test_received_synchronize_connection(): + test_setup = create_test_setup() + test_setup.reset_mocks() + test_setup.background_peer_state.received_synchronize_connection() + assert ( + test_setup.synchronize_connection_sender_mock.mock_calls == [] + and test_setup.peer_is_ready_sender_mock.mock_calls == [call.enable(), call.reset_timer()] + and test_setup.abort_timeout_sender_mock.mock_calls == [call.stop()] + and test_setup.sender_mock.mock_calls == [ + call.send(Message(__root__=AcknowledgeConnectionMessage(source=test_setup.my_connection_info)))] + and mock_cast(test_setup.socket_factory_mock.create_socket).mock_calls == [] + and test_setup.receive_socket_mock.mock_calls == [] + ) + + +def test_received_acknowledge_connection(): + test_setup = create_test_setup() + test_setup.reset_mocks() + test_setup.background_peer_state.received_acknowledge_connection() + assert ( + test_setup.synchronize_connection_sender_mock.mock_calls == [call.stop()] + and test_setup.peer_is_ready_sender_mock.mock_calls == [call.send_if_necessary(force=True)] + and test_setup.abort_timeout_sender_mock.mock_calls == [call.stop()] + and test_setup.sender_mock.mock_calls == [] + and mock_cast(test_setup.socket_factory_mock.create_socket).mock_calls == [] + and test_setup.receive_socket_mock.mock_calls == [] + ) diff --git a/tests/udf_communication/peer_communication/test_peer_is_ready_sender.py b/tests/udf_communication/peer_communication/test_peer_is_ready_sender.py new file mode 100644 index 00000000..5c3412dd --- /dev/null +++ b/tests/udf_communication/peer_communication/test_peer_is_ready_sender.py @@ -0,0 +1,170 @@ +import dataclasses +from typing import Union, cast, Any +from unittest.mock import MagicMock, Mock, create_autospec, call + +from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo +from exasol_advanced_analytics_framework.udf_communication.ip_address import IPAddress, Port +from exasol_advanced_analytics_framework.udf_communication.messages import PeerIsReadyToReceiveMessage +from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.peer_is_ready_sender import \ + PeerIsReadySender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.timer import Timer +from exasol_advanced_analytics_framework.udf_communication.serialization import serialize_message +from exasol_advanced_analytics_framework.udf_communication.socket_factory.abstract import Socket + + +def mock_cast(obj: Any) -> Mock: + return cast(Mock, obj) + + +@dataclasses.dataclass() +class TestSetup: + peer: Peer + timer_mock: Union[MagicMock, Timer] + out_control_socket_mock: Union[MagicMock, Socket] + peer_is_ready_sender: PeerIsReadySender = None + + def reset_mock(self): + self.out_control_socket_mock.reset_mock() + self.timer_mock.reset_mock() + + +def create_test_setup(): + peer = Peer( + connection_info=ConnectionInfo( + name="t2", + ipaddress=IPAddress(ip_address="127.0.0.1"), + port=Port(port=12), + group_identifier="g" + )) + my_connection_info = ConnectionInfo( + name="t1", + ipaddress=IPAddress(ip_address="127.0.0.1"), + port=Port(port=11), + group_identifier="g" + ) + timer_mock = create_autospec(Timer) + out_control_socket_mock = create_autospec(Socket) + peer_is_ready_sender = PeerIsReadySender( + peer=peer, + my_connection_info=my_connection_info, + out_control_socket=out_control_socket_mock, + timer=timer_mock + ) + return TestSetup( + peer=peer, + timer_mock=timer_mock, + out_control_socket_mock=out_control_socket_mock, + peer_is_ready_sender=peer_is_ready_sender + ) + + +def test_init(): + test_setup = create_test_setup() + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [] + ) + + +def test_send_if_necessary_after_init_and_is_time_false(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = False + test_setup.reset_mock() + + test_setup.peer_is_ready_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [ + call.is_time() + ] + ) + + +def test_send_if_necessary_after_init_and_is_time_false_and_force(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = False + test_setup.reset_mock() + + test_setup.peer_is_ready_sender.send_if_necessary(force=True) + + assert ( + test_setup.out_control_socket_mock.mock_calls == + [ + call.send(serialize_message(PeerIsReadyToReceiveMessage(peer=test_setup.peer))) + ] + and test_setup.timer_mock.mock_calls == [ + call.is_time() + ] + ) + + +def test_send_if_necessary_after_init_and_is_time_true(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.reset_mock() + + test_setup.peer_is_ready_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [ + call.is_time() + ] + ) + + +def test_send_if_necessary_after_enable_and_is_time_false(): + test_setup = create_test_setup() + test_setup.peer_is_ready_sender.enable() + mock_cast(test_setup.timer_mock.is_time).return_value = False + test_setup.reset_mock() + + test_setup.peer_is_ready_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [call.is_time()] + ) + + +def test_send_if_necessary_after_enable_and_is_time_true(): + test_setup = create_test_setup() + test_setup.peer_is_ready_sender.enable() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.reset_mock() + + test_setup.peer_is_ready_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == + [ + call.send(serialize_message(PeerIsReadyToReceiveMessage(peer=test_setup.peer))) + ] + and test_setup.timer_mock.mock_calls == [call.is_time()] + ) + + +def test_send_if_necessary_after_enable_and_is_time_true_twice(): + test_setup = create_test_setup() + test_setup.peer_is_ready_sender.enable() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.peer_is_ready_sender.send_if_necessary() + test_setup.reset_mock() + + test_setup.peer_is_ready_sender.send_if_necessary() + + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [call.is_time()] + ) + + +def test_reset_timer(): + test_setup = create_test_setup() + test_setup.peer_is_ready_sender.reset_timer() + assert ( + test_setup.out_control_socket_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [call.reset_timer()] + ) diff --git a/tests/udf_communication/peer_communication/test_send_recv.py b/tests/udf_communication/peer_communication/test_send_recv.py index 5ccb2748..d2fb6fc5 100644 --- a/tests/udf_communication/peer_communication/test_send_recv.py +++ b/tests/udf_communication/peer_communication/test_send_recv.py @@ -6,11 +6,14 @@ import structlog import zmq from structlog import WriteLoggerFactory +from structlog.tracebacks import ExceptionDictTransformer +from structlog.typing import FilteringBoundLogger from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo from exasol_advanced_analytics_framework.udf_communication.ip_address import IPAddress from exasol_advanced_analytics_framework.udf_communication.peer_communicator import PeerCommunicator from exasol_advanced_analytics_framework.udf_communication.socket_factory.zmq_wrapper import ZMQSocketFactory +from tests.udf_communication.peer_communication.conditional_method_dropper import ConditionalMethodDropper from tests.udf_communication.peer_communication.utils import TestProcess, BidirectionalQueue, assert_processes_finish structlog.configure( @@ -18,14 +21,17 @@ logger_factory=WriteLoggerFactory(file=Path(__file__).with_suffix(".log").open("wt")), processors=[ structlog.contextvars.merge_contextvars, + ConditionalMethodDropper(method_name="debug"), structlog.processors.add_log_level, - structlog.processors.StackInfoRenderer(), - structlog.dev.set_exc_info, structlog.processors.TimeStamper(), + structlog.processors.ExceptionRenderer(exception_formatter=ExceptionDictTransformer(locals_max_string=320)), + structlog.processors.CallsiteParameterAdder(), structlog.processors.JSONRenderer() ] ) +LOGGER: FilteringBoundLogger = structlog.get_logger() + def run(name: str, group_identifier: str, number_of_instances: int, queue: BidirectionalQueue, seed: int = 0): listen_ip = IPAddress(ip_address=f"127.1.0.1") @@ -42,6 +48,7 @@ def run(name: str, group_identifier: str, number_of_instances: int, queue: Bidir for index, connection_infos in peer_connection_infos.items(): com.register_peer(connection_infos) com.wait_for_peers() + LOGGER.info("Peer is ready", name=name) for peer in com.peers(): com.send(peer, [socker_factory.create_frame(name.encode("utf8"))]) received_values: Set[str] = set() @@ -53,17 +60,40 @@ def run(name: str, group_identifier: str, number_of_instances: int, queue: Bidir @pytest.mark.parametrize("number_of_instances, repetitions", [(2, 1000), (10, 100)]) def test_reliability(number_of_instances: int, repetitions: int): - for i in range(repetitions): - group = f"{time.monotonic_ns()}" - expected_received_values, received_values = run_test(group, number_of_instances) - assert expected_received_values == received_values + run_test_with_repetitions(number_of_instances, repetitions) + + +REPETITIONS_FOR_FUNCTIONALITY = 2 + + +def test_functionality_2(): + run_test_with_repetitions(2, REPETITIONS_FOR_FUNCTIONALITY) -def test_functionality(): - group = f"{time.monotonic_ns()}" - number_of_instances = 2 - expected_received_values, received_values = run_test(group, number_of_instances) - assert expected_received_values == received_values +def test_functionality_10(): + run_test_with_repetitions(10, REPETITIONS_FOR_FUNCTIONALITY) + + +def test_functionality_25(): + run_test_with_repetitions(25, REPETITIONS_FOR_FUNCTIONALITY) + + +def run_test_with_repetitions(number_of_instances: int, repetitions: int): + for i in range(repetitions): + LOGGER.info(f"Start iteration", + iteration=i + 1, + repetitions=repetitions, + number_of_instances=number_of_instances) + start_time = time.monotonic() + group = f"{time.monotonic_ns()}" + expected_peers_of_threads, peers_of_threads = run_test(group, number_of_instances) + assert expected_peers_of_threads == peers_of_threads + end_time = time.monotonic() + LOGGER.info(f"Finish iteration", + iteration=i + 1, + repetitions=repetitions, + number_of_instances=number_of_instances, + duration=end_time - start_time) def run_test(group: str, number_of_instances: int): @@ -75,7 +105,7 @@ def run_test(group: str, number_of_instances: int): connection_infos[i] = processes[i].get() for i in range(number_of_instances): t = processes[i].put(connection_infos) - assert_processes_finish(processes, timeout_in_seconds=120) + assert_processes_finish(processes, timeout_in_seconds=180) received_values: Dict[int, Set[str]] = {} for i in range(number_of_instances): received_values[i] = processes[i].get() diff --git a/tests/udf_communication/peer_communication/test_synchronize_connection_sender.py b/tests/udf_communication/peer_communication/test_synchronize_connection_sender.py new file mode 100644 index 00000000..635bfc07 --- /dev/null +++ b/tests/udf_communication/peer_communication/test_synchronize_connection_sender.py @@ -0,0 +1,179 @@ +import dataclasses +from typing import Union +from unittest.mock import MagicMock, create_autospec, call + +from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo +from exasol_advanced_analytics_framework.udf_communication.ip_address import IPAddress, Port +from exasol_advanced_analytics_framework.udf_communication.messages import SynchronizeConnectionMessage, \ + Message +from exasol_advanced_analytics_framework.udf_communication.peer import Peer +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.sender import Sender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.synchronize_connection_sender import \ + SynchronizeConnectionSender +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.timer import Timer +from tests.udf_communication.peer_communication.mock_cast import mock_cast + + +@dataclasses.dataclass() +class TestSetup: + my_connection_info: ConnectionInfo + timer_mock: Union[MagicMock, Timer] + sender_mock: Union[MagicMock, Sender] + synchronize_connection_sender: SynchronizeConnectionSender + + def reset_mocks(self): + self.sender_mock.reset_mock() + self.timer_mock.reset_mock() + + +def create_test_setup(): + peer = Peer( + connection_info=ConnectionInfo( + name="t2", + ipaddress=IPAddress(ip_address="127.0.0.1"), + port=Port(port=12), + group_identifier="g" + )) + my_connection_info = ConnectionInfo( + name="t1", + ipaddress=IPAddress(ip_address="127.0.0.1"), + port=Port(port=11), + group_identifier="g" + ) + timer_mock = create_autospec(Timer) + sender_mock = create_autospec(Sender) + synchronize_connection_sender = SynchronizeConnectionSender( + sender=sender_mock, + timer=timer_mock, + my_connection_info=my_connection_info, + peer=peer + ) + return TestSetup( + sender_mock=sender_mock, + timer_mock=timer_mock, + my_connection_info=my_connection_info, + synchronize_connection_sender=synchronize_connection_sender + ) + + +def test_init(): + test_setup = create_test_setup() + assert ( + test_setup.sender_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [] + ) + + +def test_send_if_necessary_after_init_and_is_time_false(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = False + test_setup.reset_mocks() + + test_setup.synchronize_connection_sender.send_if_necessary() + + assert ( + test_setup.sender_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [call.is_time()] + ) + + +def test_send_if_necessary_after_init_and_is_time_false_and_force(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = False + test_setup.reset_mocks() + + test_setup.synchronize_connection_sender.send_if_necessary(force=True) + + assert ( + test_setup.sender_mock.mock_calls == + [ + call.send(Message(__root__=SynchronizeConnectionMessage(source=test_setup.my_connection_info))) + ] + and test_setup.timer_mock.mock_calls == + [ + call.is_time(), + call.reset_timer() + ] + ) + + +def test_send_if_necessary_after_init_and_is_time_true(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.reset_mocks() + + test_setup.synchronize_connection_sender.send_if_necessary() + + assert ( + test_setup.sender_mock.mock_calls == + [ + call.send(Message(__root__=SynchronizeConnectionMessage(source=test_setup.my_connection_info))) + ] + and test_setup.timer_mock.mock_calls == + [ + call.is_time(), + call.reset_timer() + ] + ) + + +def test_send_if_necessary_twice_and_is_time_true(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.synchronize_connection_sender.send_if_necessary() + test_setup.reset_mocks() + + test_setup.synchronize_connection_sender.send_if_necessary() + + assert ( + test_setup.sender_mock.mock_calls == + [ + call.send(Message(__root__=SynchronizeConnectionMessage(source=test_setup.my_connection_info))) + ] + and test_setup.timer_mock.mock_calls == + [ + call.is_time(), + call.reset_timer() + ] + ) + + +def test_received_acknowledge_connection_after_init(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.reset_mocks() + + test_setup.synchronize_connection_sender.stop() + + assert ( + test_setup.sender_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [] + ) + + +def test_received_acknowledge_connection_after_send(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.synchronize_connection_sender.send_if_necessary() + test_setup.reset_mocks() + + test_setup.synchronize_connection_sender.stop() + + assert ( + test_setup.sender_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [] + ) + + +def test_send_if_necessary_after_received_acknowledge_connection_and_is_time_true(): + test_setup = create_test_setup() + mock_cast(test_setup.timer_mock.is_time).return_value = True + test_setup.synchronize_connection_sender.stop() + test_setup.reset_mocks() + + test_setup.synchronize_connection_sender.send_if_necessary() + + assert ( + test_setup.sender_mock.mock_calls == [] + and test_setup.timer_mock.mock_calls == [call.is_time()] + ) diff --git a/tests/udf_communication/peer_communication/test_timer.py b/tests/udf_communication/peer_communication/test_timer.py new file mode 100644 index 00000000..d8187b44 --- /dev/null +++ b/tests/udf_communication/peer_communication/test_timer.py @@ -0,0 +1,82 @@ +from typing import Union +from unittest.mock import create_autospec, MagicMock, call + +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.clock import Clock +from exasol_advanced_analytics_framework.udf_communication.peer_communicator.timer import Timer +from tests.mock_cast import mock_cast + + +def test_init(): + clock_mock: Union[MagicMock, Clock] = create_autospec(Clock) + + timer = Timer(clock=clock_mock, timeout_in_ms=10) + + assert clock_mock.mock_calls == [call.current_timestamp_in_ms()] + + +def test_is_time_false(): + clock_mock: Union[MagicMock, Clock] = create_autospec(Clock) + mock_cast(clock_mock.current_timestamp_in_ms).side_effect = [0, 10] + timer = Timer(clock=clock_mock, timeout_in_ms=10) + clock_mock.reset_mock() + + result = timer.is_time() + + assert result == False and clock_mock.mock_calls == [call.current_timestamp_in_ms()] + + +def test_is_time_true(): + clock_mock: Union[MagicMock, Clock] = create_autospec(Clock) + mock_cast(clock_mock.current_timestamp_in_ms).side_effect = [0, 11] + timer = Timer(clock=clock_mock, timeout_in_ms=10) + clock_mock.reset_mock() + + result = timer.is_time() + + assert result == True and clock_mock.mock_calls == [call.current_timestamp_in_ms()] + +def test_is_time_true_after_true(): + clock_mock: Union[MagicMock, Clock] = create_autospec(Clock) + mock_cast(clock_mock.current_timestamp_in_ms).side_effect = [0, 11, 12] + timer = Timer(clock=clock_mock, timeout_in_ms=10) + timer.is_time() + clock_mock.reset_mock() + + result = timer.is_time() + + assert result == True and clock_mock.mock_calls == [call.current_timestamp_in_ms()] + + +def test_reset_timer(): + clock_mock: Union[MagicMock, Clock] = create_autospec(Clock) + mock_cast(clock_mock.current_timestamp_in_ms).side_effect = [0, 11] + timer = Timer(clock=clock_mock, timeout_in_ms=10) + clock_mock.reset_mock() + + timer.reset_timer() + + assert clock_mock.mock_calls == [call.current_timestamp_in_ms()] + + +def test_it_time_false_after_reset_timer(): + clock_mock: Union[MagicMock, Clock] = create_autospec(Clock) + mock_cast(clock_mock.current_timestamp_in_ms).side_effect = [0, 10, 20] + timer = Timer(clock=clock_mock, timeout_in_ms=10) + timer.reset_timer() + clock_mock.reset_mock() + + result = timer.is_time() + + assert result == False and clock_mock.mock_calls == [call.current_timestamp_in_ms()] + + +def test_it_time_true_after_reset_timer(): + clock_mock: Union[MagicMock, Clock] = create_autospec(Clock) + mock_cast(clock_mock.current_timestamp_in_ms).side_effect = [0, 10, 21] + timer = Timer(clock=clock_mock, timeout_in_ms=10) + timer.reset_timer() + clock_mock.reset_mock() + + result = timer.is_time() + + assert result == True and clock_mock.mock_calls == [call.current_timestamp_in_ms()] diff --git a/tests/udf_communication/peer_communication/utils.py b/tests/udf_communication/peer_communication/utils.py index 3684d1a9..2979320d 100644 --- a/tests/udf_communication/peer_communication/utils.py +++ b/tests/udf_communication/peer_communication/utils.py @@ -4,8 +4,13 @@ from queue import Queue from typing import Any, Callable, List +import structlog +from structlog.typing import FilteringBoundLogger + NANOSECONDS_PER_SECOND = 10 ** 9 +LOGGER: FilteringBoundLogger = structlog.get_logger(__name__) + class BidirectionalQueue: @@ -57,13 +62,14 @@ def assert_processes_finish(processes: List[TestProcess], timeout_in_seconds: in timeout_in_ns = timeout_in_seconds * NANOSECONDS_PER_SECOND start_time_ns = time.monotonic_ns() while True: - no_alive_processes = not any(get_alive_processes(processes)) + alive_processes = get_alive_processes(processes) + no_alive_processes = not any(alive_processes) if no_alive_processes: break difference_ns = time.monotonic_ns() - start_time_ns if difference_ns > timeout_in_ns: break - time.sleep(0.001) + time.sleep(0.01) alive_processes_before_kill = [process.name for process in get_alive_processes(processes)] kill_alive_processes(processes) if len(get_alive_processes(processes)) > 0: diff --git a/tests/udf_communication/socket_factory/zmq/test_zmq_socket.py b/tests/udf_communication/socket_factory/zmq/test_zmq_socket.py index 8e9c410b..108dfdb0 100644 --- a/tests/udf_communication/socket_factory/zmq/test_zmq_socket.py +++ b/tests/udf_communication/socket_factory/zmq/test_zmq_socket.py @@ -1,4 +1,4 @@ -import time +from typing import Union, Optional from typing import Union, Optional from unittest.mock import create_autospec, MagicMock diff --git a/tests/udf_communication/test_local_discovery.py b/tests/udf_communication/test_local_discovery.py index 00a25244..8969a816 100644 --- a/tests/udf_communication/test_local_discovery.py +++ b/tests/udf_communication/test_local_discovery.py @@ -6,6 +6,7 @@ import structlog import zmq from structlog import WriteLoggerFactory +from structlog.tracebacks import ExceptionDictTransformer from structlog.types import FilteringBoundLogger from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo @@ -16,6 +17,7 @@ from exasol_advanced_analytics_framework.udf_communication.peer_communicator import PeerCommunicator from exasol_advanced_analytics_framework.udf_communication.peer_communicator.peer_communicator import key_for_peer from exasol_advanced_analytics_framework.udf_communication.socket_factory.zmq_wrapper import ZMQSocketFactory +from tests.udf_communication.peer_communication.conditional_method_dropper import ConditionalMethodDropper from tests.udf_communication.peer_communication.utils import TestProcess, BidirectionalQueue, assert_processes_finish structlog.configure( @@ -23,10 +25,11 @@ logger_factory=WriteLoggerFactory(file=Path(__file__).with_suffix(".log").open("wt")), processors=[ structlog.contextvars.merge_contextvars, + ConditionalMethodDropper(method_name="debug"), structlog.processors.add_log_level, - structlog.processors.StackInfoRenderer(), - structlog.dev.set_exc_info, structlog.processors.TimeStamper(), + structlog.processors.ExceptionRenderer(exception_formatter=ExceptionDictTransformer(locals_max_string=320)), + structlog.processors.CallsiteParameterAdder(), structlog.processors.JSONRenderer() ] ) @@ -61,20 +64,40 @@ def run(name: str, group_identifier: str, number_of_instances: int, queue: Bidir @pytest.mark.parametrize("number_of_instances, repetitions", [(2, 1000), (10, 100)]) def test_reliability(number_of_instances: int, repetitions: int): + run_test_with_repetitions(number_of_instances, repetitions) + + +REPETITIONS_FOR_FUNCTIONALITY = 2 + + +def test_functionality_2(): + run_test_with_repetitions(2, REPETITIONS_FOR_FUNCTIONALITY) + + +def test_functionality_10(): + run_test_with_repetitions(10, REPETITIONS_FOR_FUNCTIONALITY) + + +def test_functionality_25(): + run_test_with_repetitions(25, REPETITIONS_FOR_FUNCTIONALITY) + + +def run_test_with_repetitions(number_of_instances: int, repetitions: int): for i in range(repetitions): + LOGGER.info(f"Start iteration", + iteration=i + 1, + repetitions=repetitions, + number_of_instances=number_of_instances) + start_time = time.monotonic() group = f"{time.monotonic_ns()}" expected_peers_of_threads, peers_of_threads = run_test(group, number_of_instances) assert expected_peers_of_threads == peers_of_threads - - -def test_functionality(): - number_of_instances = 2 - group = f"{time.monotonic_ns()}" - logger = LOGGER.bind(group=group, location="test") - logger.info("start") - expected_peers_of_threads, peers_of_threads = run_test(group, number_of_instances) - assert expected_peers_of_threads == peers_of_threads - logger.info("success") + end_time = time.monotonic() + LOGGER.info(f"Finish iteration", + iteration=i + 1, + repetitions=repetitions, + number_of_instances=number_of_instances, + duration=end_time - start_time) def run_test(group: str, number_of_instances: int): @@ -84,7 +107,7 @@ def run_test(group: str, number_of_instances: int): for i in range(number_of_instances): processes[i].start() connection_infos[i] = processes[i].get() - assert_processes_finish(processes, timeout_in_seconds=120) + assert_processes_finish(processes, timeout_in_seconds=180) peers_of_threads: Dict[int, List[ConnectionInfo]] = {} for i in range(number_of_instances): peers_of_threads[i] = processes[i].get() diff --git a/tests/udf_communication/test_messages.py b/tests/udf_communication/test_messages.py index e9dcd769..c5dafe59 100644 --- a/tests/udf_communication/test_messages.py +++ b/tests/udf_communication/test_messages.py @@ -3,7 +3,7 @@ from exasol_advanced_analytics_framework.udf_communication.connection_info import ConnectionInfo from exasol_advanced_analytics_framework.udf_communication.ip_address import Port, IPAddress from exasol_advanced_analytics_framework.udf_communication.messages import RegisterPeerMessage, Message, PingMessage, \ - StopMessage, PayloadMessage, MyConnectionInfoMessage, WeAreReadyToReceiveMessage, AreYouReadyToReceiveMessage, \ + StopMessage, PayloadMessage, MyConnectionInfoMessage, \ PeerIsReadyToReceiveMessage from exasol_advanced_analytics_framework.udf_communication.peer import Peer from exasol_advanced_analytics_framework.udf_communication.serialization import serialize_message, deserialize_message @@ -19,8 +19,6 @@ StopMessage(), PayloadMessage(source=connection_info), MyConnectionInfoMessage(my_connection_info=connection_info), - WeAreReadyToReceiveMessage(source=connection_info), - AreYouReadyToReceiveMessage(source=connection_info), PeerIsReadyToReceiveMessage(peer=peer) ] @@ -35,6 +33,7 @@ def test_message_serialization(message): obj = deserialize_message(byte_string, Message) assert message == obj.__root__ + @pytest.mark.parametrize( "message", [message for message in messages],