From 18442eeacf1ece7fcf4d5268b03c1f4e9229e90c Mon Sep 17 00:00:00 2001 From: elijahbenizzy Date: Mon, 3 Feb 2025 21:12:06 -0800 Subject: [PATCH] Updates from_functions and from_molues to use the same base "compile" function This makes it so that we reduce code paths -- we'll eventually use compile for everything. This also allows us to push functions up to the user level, enabling `with_functions` Note the following desired behavior: Desired behavior: - .with_modules() is the main entrypoint, users pass code that's stored in versionable file - .allow_module_overrides() (current behavior) allows .with_modules(module_a, module_b) to have module_b override nodes from module_a - .with_functions() allows to build a DAG from functions alone or in combination with .with_modules(). Node names must be unique (current behavior) - .with_functions() + .allow_module_overrides() means the content from .with_functions() is applied last. - nodes produced by .with_functions() are overriden in order if duplicates --- dag_example_module.png | Bin 27367 -> 27325 bytes hamilton/async_driver.py | 4 ++- hamilton/driver.py | 24 +++++++++++++--- hamilton/graph.py | 58 +++++++++++++++++++++++++++++---------- pyproject.toml | 3 +- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/dag_example_module.png b/dag_example_module.png index 5351fb719b68ca8d7ce09d441a72e4593589718a..6bacf4647d72389dd1078cfc22bc333c839453f9 100644 GIT binary patch delta 25297 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 U>X1B#4E`v|tKa>8+vN5C0Q_;cRsaA1 delta 25259 zcmagGbzD{J_BXnatpXw-p&}q6DM&X8C?Flu4FZCI)S`PDgeW1Yw19MXhteP+-5@C~ zB@K71ea`!Se)rzbd++&UpU;NHTyxIndB*tG*tLULOM_S+MQvR?6_%(QwtBk4Bxvu7 zzawjsRpE}r%hlISeHT+0Nt*g3JA-v>Wbjt%fO+Y_5YsJ>mUlixH1E`L#r{^+i}p<< zw%OU*&zX5n=W80U3oZ%v9EvP8olZF6gD)> zl`Soqk&~^<`w)>Stw|@Vm)%Oo$zi{D#R*fsCQliKpTJ zoq+S&O_SE(gXYm?~|L$y0Njb zHd)QDTKVb~YGQJ-u)I9|<#1k(ZaET+eVL#)MT+n)EiG@B?eE?sS2s8G`I(pd{)WOr zM`Wf(?ya|%uHI!}U=SZIe$mqhAI;Lpm0cam)h@FlmrdnD2>gh|o{KZG3*LVR@5^E+ zhdhK@Gz~dF`y3lPx4J5~)E&RIwPmz6Qi!4ywDHl-a9C0haGP35OTwSa`J3A@x^fxIe z<~BBh6B2H#rpYHcF82vNFoU-xdwA*MrAy3Mrq7iR4`t*tT>1|qIWqg~|- z9jfVynNjp~bWFNsR+&5q7nh2pq$IPdib{mj>X5pQmKIav_wQ;8um}I7IO-(+wb%&RH#jsw%liS$X zNIZFR?e^{NOx5%X$7Nz#so<}&k<3}^l$4aehw}}+DT!%(+M*`1F68M~`>ABo2?+^_ zUqGpaEBgBRsbr}k_fVEYInkNHpO$VYm)AM+@$tcXeZlm?$O!9^qa)9^Z{JV@*_!OF zr+73?v3CNn)u_hkWg{ATdpR0~Arg{sD-(ES z58bx^L3w$d%C8zK2#Dc?PSc@L>e(6>`)p3FMha*et0IsHQiB@T*O{63&QEp-QLQ1= z=?+WXj2^(OSFT=X~IT;xlSq{_s5RKjOqI|TsGFVtv)-pW&3BDA6 zb+J4CgRk$!7u|8++S_rlv9Zt2jyLg5ZKi74Ut?e9$y3q~y2bmwxA!BwWmBz$xVRr_ z3b6xuh!hS@Nz#ZO+H-%>CA#i3UAN9t5bnUV?IX?Z`uL{+5=MAS_ng;8zU62aoA;+( zg;&l*iq{fI{x&3pFd-qq)zy`VnE3ntG?JJkLQ6|qx}vJt{GN=Q*OK(Zbynj)Gv5Ml z@#6W4ecN5`4|xB+qaS@u3@50zol~!hGB_6LS5i?4Q^{0g{&ZhEnTL;0gO#6y<7x0s zPBC6b4ft_*SXc!2i*7X?9UW$@RQR9$?(d%+u-eR63veFQOb0Xbl~W`;bV@Bv-#@6W z+cheK)7FtB?r-+48s2q>q4(hvzd1absjp;ssoPz`?v$jYKH=dcuV25u=*)xk!fC6p z`*q>x&z}Mh9;g-?H(Pazcpa(n9xX#jGTq;poTzeQ8Q#mB+nB7@tMiN=D}9-LBr^52 zqC!AkL1DDg;YFJFZ5oNff})X$ER|2u(d3)8`vy^~v$M12qeb+2d3pEsDxOC27#kb^ z+1u-~udq_wTabh|v=is)LxkLAr$d`Jz^>*p?I>_uR!L4y{ymUM0>#gCeXMk%+9hN# zTXR)N0(P-Wm;g@+9Gb-+(T&r{c~_6OED74r zjl~mOIbD7KeaU8e20QxuarI{+4PRx#p}u|ZcR-o61ctUJ z`Vi@;nv+P$$|86yntAi9`RO0ugz-`D&DIj=XX;cxQ<{2nfg%ClMpH_bm#c<>86Abd`e3?1z?)<7)d#z0!=}8H>y<_QGrxvXE;j z+P^#k4QN}rY|lI`hx59kxnDR^#SkTbWN{3bYYU~V*-=DHW>t+w^RV$xjJX)-r~90= zE=IjhqM^P@-X3xs{7g0V)+WDg_h$Wh-fDplk*`*gETwL_O{!BY-+(jPbb}pvrkR?R zC(g{%p*7B7dZ;tze14OVknlRI&ezqC@CRy1%X0!=M?7A~dnwlYA>^E<*Wo-2(~t?d zA06SxK18sy?H9;Jy&pz0tAC%I)W^JByl63WGkSAM7`heO-tE(`@E6j3ZM6H3X@CDg zBS*XIEMB+a7?Hhi;(YdsQPMWjc=r3k?{)`V9nRfs+l>AOU+>e)A0r~ZuG+yJNWWI1 z951t;>rHwa7{@0i^sQ9daQfBH;<~x+`0@LQc9F#JFB}@Y&DW`e|EZWsP9kf!$gPYID2rqKaE@O5^^@juq+gDH~Y9Z^f&pf z0^?>pegC`v`G{7vGi#7;S#w8OT%P#7BH5dT_h-OnW$E z6@fU8G7jlT6B7FItTs|zI@1J77@`YU{Ab{uPv^M5jd|KXH&AMJE|2DEgB^?-&uCjMaNRCpIJ#MZq zbFO4{G`25l2zBze(KtcWgCPLEnu%<*3B7kBNQ}(Qk#lUwx#Lr$77M)TjyGzYwRKB( zd#OJAd%)#Rz}A+~2N@ii?c_Fw;NMF34;0HZfv82N@ma#Gi=7=0Dv%EG-KRLReE-b5R9ltRX@A7g09<=r2n4F32xz zPS-V)opUp(_UGNQF}OOLGyPa(^=D`+;pUD(Yo69<)KWv)v=)a^pk;OE3q`J}6z*UA zw!LK@>{FWqK_2B|(TDAi@(cw0^&_*iw8l|S>txkC)XOg4IY<`$)RUkx=+JzQpXjvN zHi`67l#$r-MnQ%i?m*ZtIN8#=RsL9+ITFeF9Iw$+9$T*3l4hkNEH24qElurxYQ)(k2<8FtkSiat(wI;uMABuy6_(nv_t#IK@;Q@n=gP^-dt7Ym+{vkD zV}n*^+suY$QdQ+01y@Mr0I>B_yAnz+&)Q*-K?><8G- zbPK~;ws2`AyzglQq*(dBe^}-6X@36sV9^8qyq%f9tU;?;zw~aD~en_d(H>y`3)t?_L7b5=v&FW@pJO8XD)QjeII#{1pYw+Nj{*y^?4n_T>0;hds{1xm`} z!WGI*K68O@H%_<4ky@(y$??MbFXcM(sIe>vu`IKuKeMCgb&;b`uX=0Lm%!4iiAZ^1 zj8ng)`k;$Ph>jCQfy)l&cyYL#Zbq+cQSQ@c-n+M^cTVELpRdVkJDpA4$k@~NM9!D! z?m1J0ZGjXo6B7<2BjW{ZY_#K#6Zyu@G^NDuv8xn^-ut46P6+La3|(l*ZDYgiV4Zv% zxA=n!t?zmI&AY7J9hzmW4ZR$y(TwLJ-@iVWk(aHDv=;8UOItxk>64_4n>CQrrJMR= zhcdRxEb!)|Tv=yeRcx*HXf*nBOiYo(lA@4H)F&Q1W&GUx1)7xfa#3_NY;1-reC#Fx zZsc;ggDfwz+SO-m|uN;#Iz1N){zKYhuF5&E4N#=S$=(_j-MkSPc;pHDdZ5-t~S31qyUbM;eDBgtUiSv`RKI*<azhSrYBRs z^pV-cAyO6=mVZ0iKX_KNsGEFNH~I7aF>`NIH1B!)z$i|rBoPD9g-B^6Xq3N@v{rA{ z$@vpdY4dtyIfgBT4?5H*0dd-08&&;p&x;p80W@f<9WOsvEf1<9NJZMVL|0mu5rRLb zMW#1zl8%IqioLs#*1$+``SN8EYSDzm`dEeEk&pEKRi8^dtmp}ApIcu3^%&5#BULa_ zGSR(10ocy~6qcAzy@^hA?Rfq~hlrY@F)$Rnu(0I$!h5nR0x2BjHq!S^N%{FbEkP6o zMFr1}{+xw9+`hAT=wOBfQA#+@n*iVZP@=f=s3MSoMoJdbg!Q!2-c&K+p-iLu-a*l{l-23- z<*@KDM=Fii>GBE-P_A!o{={W*;Oo>zO($IMu{RUNdc35#P)X_=II#?W~p0E7rtd>iMsI2+}x)yGc%ahvzT~xPFh|X^(7~l zRlnNNXl^VK!(D@E0g2N43ZcJdtELlNxxx1Q*LPnZe}9sjH>D*c{wXXhtRh$s;^O0L z`TqTal$6xk`g$psOeB$5O_nqW73vvePhW!s@LLF<#)qj0Pk*U`NDOB(EPRceF`VP_ zt?&bKivRvU0fMc6`J<*($Zb$2SOlZU1?=tsM<2~QKU_+bk(OpE8rRHt`Z}2EfmyWy z4h>JzQ(*1jTq1t70 zF*6vzFxlujF!~?y4_vm@>fy<9dKn2qi$O4175axp;u>xITxcP+xUUErxX?#j5{Cag zEFa#f-9)x#zWvUk0symTPb4Hh#>EBZ=H~tj{jT|*y%(Qf%g(P$wwrbC=fB$kcmHQO6S^}zY#$B&PC5`>+o z-fOgbQi|bNhl>QC|A^uJAQKL-6<;w<07GnDcYlXszVqGvcCuEarltn3i8Ve&Dg;Ow zsgTo(#AuPJ6Pz`%=XV3<5K-()_`OazVgTXVa?M-r!M zJyhW=WT|Jje0lu#T|j_xo?#_={Ut{MY5xj}66Ow+L=JuY zu2N)`)&pgYi-;hF8qp4PFhesxVQ8GrhsaI+cfLTLL2U;x(4ABp5)zWS!$k_XILh7K zy`r-68X4IWSJ#Rxz2&}Ca^!(?&Wek$uyEbc3QL46{qg>0&!>?AGLXy~{Ry3Jy@m31 zZ#RHebaZ6onUz(XQ8efxDB$;5rUV28S@U7tKYw16uDYCqt=YcY5Rm0wvddU7$U$vffsM5ar|R8wxCpnS_)y!n6-I*c*Ac6eu=-e)&^x z1D7QxC2jf<&8=PZ99P)m?^o?&GZc@_#OJ0aiHE|%TRS@@p!0Yh{PG7e@kIvk#10ga z1G*YWA9Y)0K;u_QkA78Le4MS3TTon_;<3MxHLlzh%m3nVdm)Q!t~F$7zCGM(tRw&g zRHGL!r0Ts-UzPZy`b#bPFKKFOdLR6{jCt>(pgx&VfV7mK^t5iGGq&f=n>Tj$_IE*w z;ay?M2YMCo;X{h|>5)#R5~8Z1p#e)?U^^`Wtf~WqJ653-0%n0>w(M0SCE@@GF5SuGji_lYEoXb z5Fxm7<;qxzMYsw1%as0r;o61hGGq)*#ZOrb4@4Vhr-1zpQfj5G9W= z?Z(f6%(}4H6yE}q{Aq(fJifGR`S*LBmxP2w*(xA*ekeh%lb)1taN4#6Lx#K5l}F0+;aml; zf>5LC>wW3bmRURgD=|_Jmei2YHS*A2Qr`H7sNbL&KYjY~U&;L5JsFHx<0#J zRW*_e55Vf0q7-}un#JyXI~pD@CVZ8g3DY7FiMdadem*{b+8Gm{SJwgTxNlgeSN3Ur z$JB$yXTwyM5(^85pVkjrc>FH83#VS<<3Z&eCPMaj^Q4LK45mBrpXiEI7G47$BA#zdR<#daIg1mVm)jKaDqyko z59x7%(nzc6)IsJ?q*LI5?At);cBLc_Xnpt7acwT3Fd#Fpk@#--yUY8d6+isnB4>*I zW?fp9#us^C4h?+8g97lbCWiY3fqs=^lI5i2NyR1($UgUE#7VU@7CN}i$_Pd3*JnX| z;jvlk0L?8|nJP>`;x+Njn{Ch?9I5W2T-$m0 z9xDM!P|MRJ*VG)QEN^#gRxY>EhaLUN_A#FF2$isV1oT~w-yibP)_bqvPsLa+DaFa^ zYw}k)IUwuYQqjaxT`|1T0%+s9i;NORQDI@<(-XaA8`HWD?PfSIs?QJ*CvNXEm01#! zXb~^>Zqw%_iiT_=H1V(t^JD-8r%8+v7(*Pk;DG)?&dFFrcy?}JlmYA)z16WM1vOry??zImutWAEbP*J}XdS!t(lsl97IQ-Uxm0nluI+ z&NqDTxV38U+{~<97p#YVcY&URg9s{R2NK1az4~y(H~F}hCUUx6KfEbUYa$yHy`a_dbzV1ugHJhIQDI&xHsR!0dfnuxXgEGR z;0n@)K2EGaL;lx2aelF`OW+6cKy@9Fe(8wJDUC}erBZa_p9}MN* z<)p!SWgnR}Ge0YvuOz8jCPt5HsVJW_I-r``JfPv7%4^Lt93pmK;Gz#yF#p*cPh+Uq z+4~rI-a|0t`QXDle?kc8J1BO(YW?tM8m`wwB^B|H>fG_U zp)%~%8*V$GZXkLUdCqz(3E||l4{?Q%OW8#W2j0Vv2mea#Z#*yE$G=OR8uZW-xHK>c zldbtJl&kHCWaO<<9-*`4)+|d_sl~i={nC8HPwcZjiTw;ZM~G1fwP}tuk4WB_+B4!- zz~P`JetFT8P(`p1RPA-lw>q59NJr=M`SVR60T$zBtl)JGu6V!S;T$SW^ja)9T~dEE z_Him={`u_*+_`V@92a^LW?Q|yf3H#yQJlW{ywoGO>hjU6{w#SVPX1lUTs|v>o83NN z@$ea5L$FgeqRqQcF(^Xn?*8yY;Ik$6m#*+j)jGqs33VczJ&7cHbYa;yy3t3y-heRG z$N!0c5VZdyv0z~DIz0+?2PGr%Sj~0Z5Jfpz{^4*=GV5^QZGmK(U}?TDwb5a@Tc{#sagqNg`OF`dtL@!~~J`*~?VneAoP;{e5l zP@$or@}D_gL)+po@4a(&Fh`iXmM?I7?#&%~`itV?;8o6>iw#4 z8RK`n>LB2)L|j%G{P(ST`q6D6d{{s9iUU@HZC{eg*>uZPVc0ivd>ys!{x``hzhrxG zMZkgk08jIz=XvGiYA|9U_cCbg0r`U6O4M5hnmD|BAxND6sy8pHd1 zT!}?mLETs_S;Eg6kdfw-3QV6n(>cwr|1{zb?Npu zCpw;{TQ|$zyhWbr&ChNE+yD{|gpHe7-Y)Kpa|20=Y(Wl#e@|?}qunfm!Y|El#35fj z?Dor6rg`I5&9pxC^L)>La8|ijH5WrYB|ZBtB;*BvTD5GAAYyvinVFdeaJSvIW_`1M z{Qj8&RQ{RYI+*TuE-tN8QwiW!7uie-5l{*|3b`%(E->()XaC1+!eZ*N&*|}gx-BwY z7i}ENZ+jaWxvH0dhpQ{ci*8DwxUT`G4Gj$m2no%uugCwI&+1PbuJqtv7q!gQDdoyl zWg-P=!QsC98^1g|LD-`Od@;1W{r91v>n<+)^0oz(kRNz^nULY(!#MC-jjXNX*Eu!P zMXXCoD=2oGhw-BY$c}ZK)R4|ugYaO8NlJ@AWra$mdl$abO|zZH8Sfd7&X@X|N}l^U;nmh4YH+Zu)|lso zZY%W2R7HHMYpZqFgHHCMx_+4IJPt%`(`wi-yrMOs-CL0LC_=nX-H-NGrB09baIao9 z1rY~h*pe_RqU^TkWz@M)0^n}0kC(R&3`97ukAWH%^6AqJGG?_q(8n)cx&F+;qP_XU zjevLWo*n;P9~mF-2j^cgQKSnzUx3BN5c)tJ93Q`k9uyZ7gAB@N%gJwVFK_MdzW`!f zV*V3(W7OzZh@5*fD8xueOXGmFp;4`n-~Z(?HtG@qWt+>imlP;3oE!(|_m-z>9{_3B zJcV3?VzF75D=;Tm#MX%kO7K^OTs8v1UWb&&#XjN=f@8`!WQGJGrj9wXU!8RjmpMRN zsPj6u2bHg;wpP&bH__EwJTo)jCY)3&Z`0GF zn${g14ajS3i|IPyv$Hc;j0=#?5OQ8?ixYGZKtvo`Cjh75T)8p_stB;6IAE>zD+Bjp zz}baFi`({mK-Lp8oGn(AD8CZaw#5T1+GlISB}>$Tu-O| zuUOCNTo#h`qrWwT%gSM?#R-Ax80d2#ken!0XmZLtD754fGc&VWW|?!gxOpS=CN*IL zc|G>7g~}%pg_R_iZ>JMiuVT%NrCvFp-s`WJ%

=TNEDa5SCXY z&Z^$}_bj)5YiW$^AH&@jhr9U#H7}$RW{V2Sux6%@f1Q+?(n8ffphouQzHc>-+U#IT z&bBc6>vZhy&dwYV)T{*%rX-}KRC9F}?+LK$RWzi^eYz>)DG%PTdjIkjuwOE>$^w)LvvA&ErOoc2K8d3_ywW3AO1I_ zqvL(8$3BQ~NqMJzYM3#Cz4K!`6XZ=MNJ%>_gr}0-?D-zEipemgM` zYexI|KZ%Z3kMmt9$v1zJd$B%zn)ZRul#zdlE*`uh?E24vVo8spV>*UZ ziDxczgmMoW>wQr`3Kl-4EHsEe!IfM!8vU1?RWvyQvQS8AHySzjwrN7T0$U z9G*DOF*s5PPzrlt4_f=X#FU$LZ1ZMud9}(vYp7u71kPma`S5&7={QU++Xnl()o6Tn zdRX;UO2uhT!;FndDbbMRmhnC0Pt33Q?6Mm^e5UmLOY^2(V=iV#SywSUI70##Y)M7% zOqWG=&&$i~cXpkZ>adpwT2%)FAdMuyY9qXLbIYP^Ji|e+R3XW8VbA}6i4~hQ=Sem9 zA?5*cVxh~X0h?aMBWT6EmIE`;LPy5NAZjD_{5c&zVmtL5f;E9RIp2U1-3Lb;e-E@Z zf2cj6F;OvVzLlA<4 zi|czw2Od8^KhzP|_QR7yhaX*Cm}FCD6su~sf}o77tnqN3eiq;JRF};uD`cuB8qO8? z2xL&e<-y@7l21UvwNpU9=CPfMgEdrN@~HY0vISwH!94bSYgRmv)&1eFvd=|c1KOy} zTT(utx;=S?k%o<6)qbI{I6unhYgccX&BF?ZSjo-u`B<`5$ROO^mAz9&#$tNYcc%y{ z&c*}|&Cwyk&HP{>`3F+|VEZoL(MA6JOGf%r!9~t))k+3W$0Fi^)D0`cIUrv5Yo6X- zCH&BT=5->L=Rd*#e(?EU*84fuMeBAwUFgB4x@o=wbP_+rkldUGJLKc%mUqzREND6g z24V^dIS8r%nZLws_G1RTuOA7*v8J6-g^tLw3RLEJqgZD3ER-GSRq_FI3*+=LPnk%y zor4WQ740vg;J10MbN%}DOJDUbE~F{sb8&Dy1DH25I(m(gGR`y_ihq&Yju|FtqElvt z`&Iv{H6-Q4D?v5>Pf+eisaqpgM?Lqb1dCz{LS)u(uWRfum8Nz3zOFj>d*8o*e*TUS>{K8E4|-TnUk`vMLO ztCvHHl5M=k%(I!?r)(x9kX-5yo;51H>Ux-$;8stjtaP{1jrU!W_2fZD0=9wv_@DW$ z`kOaz%?h4n%a>Pw2=k;4UR_-D3ktdlYxzaY7jGcnFcFq0qb?QTO(k|mcXu$Hk(s%< zx9RDO09AZIU4-1vl29=Ch#*4W29=r|d=Gw`iH31xoWjAu0hAi92n89LUyX#K&q4nm z$WVR@Jh$+9JF)xUH5m-n0o2<94Lgip&JK-E0%=-4h6fkqXmB5-pg@A&V=~tg1of-8 z%IRfbVBqHmE^N?XlHeH0#|ebO0V_0VB>IMW{qsc{7{oj z_43DctzSCpBR!QU&CJX|DGkNKzSNuCd~QevPQni`D2q>Lq5)vTDL4C>`~<9Sg8zg- z#le|~M&L}O%33%Vw1iOiK+zd3u^@pP{0=V>r1Q+bdquEwF)BPr&`%$ zrr?7I4u57IL#nb7Vv^iB+?<@>`ujFaA(*d;MrG&MENfItql)t<_~w6t`Ej56QE$mlV==ORQu zUnbT*`6-i~on6DXZ;x}gVHIokr##%o($g?+DL*`M2krUi+hxb!y{`edsO0Gh<_@z< zpp~C3&bdYh1mK!>L>R$RLT>VBrYa-G-T>@h>9U!yQc5jmJ_}HBZMr@YJ=cQq>5ld{ zL#nGi$~2wD-sO;eK12izz*k|wtUk~P48jK-{KU{OQNP;RVsHF!HtcWzNc9c>p8;>! zd3ht@x13fZ9N=&)_aDSTh>ZAw7qb=_cX z?08X&?e`g`UMK*n;pydtIe)N@W(~+Itdf_*1mcY+Zp9fyzWT4DLueTn7Z+{mEZ)uD zqo?-;>`hEUGFoVI86@a$eSKlz&p6C9VC52$UP~?rS29ta0dtAiS1f6d+R5fgYES3( zZ1WZG!-X5bT;JB#dZpcv##4?XYwJcU*OUT3ZT)FgIarS%v*#Y_E{X`qZqka1z|y%e z+2Ih;vzuTYKr%Ud@3Rk6ARS1R&9L;qjv#R#GJIpG7tGNj{TI)kJ(88hgNS`ab#*`N zYW!152+jAqKNKU?z$lH|E4l$Y;SjiyO(lj4>Xh3kfRFNfsRvbGZ^ z;C-~r2rdQ40j(`9kYoP_fBFZ6u1S~ah5Y)nKAqNeP{e@oM}TbJMpCJsD*pKK<3LJ5 zVn~C4y$Dt{B)&xG?%sU^0294E-%gn*+#3NzmIPst@73`-q~YNB2pIuzw`n8@=+M7= zK_{1ymw(<7aZfGZK)-YX79K*ys+lVEU>~QrUzIetdXwYTHCjpY)VvdntUGwJ0ZygN zQUI-pNCLnlJ}0@J+#reh>Ev6C&yG@8DHLTMlM%f}RE0Xs!sbKko~>?|y-w zmDo(i0HUy1Pj!J0Ui<=OGkWMADW0dN=S`3z09+4ec)=DjgSrSw3vSB+ron>#pGb-n zx7|zwK4ij`;vMLkIw7t#QgEPKN442pQ%HL-`hoy3k%gBiVtX1X#~5N!YYJ=BK@f%RV|A#n$oW?zL3u|kW4q8=RqQu4ec$2#4jf{!l-(ee+tga3T z#c5oV&p%~(XRk#PgVTrG3Ux+@Pshu(9v(Wl6P!m>ZSoDu#)}C@FH$1m82HQ0odRW^ z+<+I;yD_~92y%vU$|dlBB_t(_o_)I@a=gZU$C@hv#uMh2mXKCRd{_WngTFF`1|BEm z^d5QvHpCC4K~NEdsU0vwzd(6_)^4{kA;_Q@*9Ld!u)lyy3P1PX zS?W;*FpYw34_SfR5T((pwD*OIyi|26BO`-hK=4&qPhmnKhR^yv5*teaXj|25CSA4M z3&7VB@B>V_zpqmw?et{szcj&i3WjU|dt^BUm!!gOJLGe+GH8pNTZ!Ikf7E?jqImY%^EhGU!Fqo;z0|D6XCej|DvdHo5~S7MQsh(C5@;u%X4h23%p0 zdg|xr2iU|Pfg{8LQ&KrOIV06B>@Zf927(m4>zSpc%U{sPD6{Uk=ml#Ig_Zt!%7EE* z$U?>)F4eR=JDG`2J%r!qSxB@x50Qa!vw@jKH>RDnTMk#a_zh5c( z&&9;VV04(ev!j={c+4Nc3&-d%;Bd|$Al5$u^-6sRn(rP>A|`0@3OLXI9=*9mNEm{@ z(07ajK@s&%D=RAwKECd!Y+mNp{~5J;G^XB~-M60&kNpN>E4@kLmr*dg)C3pMs`nNK zMcTIUoY2m!6(p(PVyAt6aDP9rC_>uFk^Cgtgka$KLA$+r~&l{;No9 z!NDRB0IbfuX&#@wc;~tMQD^T~Uu8Zc*gBe#hv>y&I(i07`q}E-jf|1-q!~sC3M@DH z;ip{gL*=&yZ$Ox?jF;ul_#Wf=6k%&$OO(4p443D&e4xRhJdt+4+tLAqEGp4+$R(J> z`Uc`6HF6RPd5B-Xf49N$RL7*XK3R;zmb(BjGN;PLFj)c@|LRrl6D=K`zI3HTeCh+} zexog4)tKG3r^}ObL~g>sP$eWsnxH!fc>J~UTY!3q8Op(okiqHL0xpUv-soeDt}u7T zo2fMEfY7}!kC>-J@v$8SS0~X#57qbA@M4RezvtpJ#lmFYXoYx|)_#fe*m)!_#*55L zK1FehkV`i#m(+2?_(u|zZlwozM30A4^L|K|K>ZFI>iB#_{>KiJ`svERCnD^=dz~8aKNr`gHlWN zJL?_K8~v`5ccelt1^`Ep!6)ngG58d9+;s&8nl9nrd=(!bzp}EZj;Sv@i(MzjoAo7D zqr|-i#{wg3wVktb%g~U<%e?^pbQ5YN0-N)AZtu$ib1tunoXOBO6aOr^=%HVZ zW0~}aWfj3{zL@&_OD#hue)J`x?$vKQp2BaSujBA++_q-20vGb+7~2grPyAy`A=WBS z2^nr~F*d`y_c%ju*$($-)YX@tNrGW(mNO;&Hq8Rjq|raRYAoTusLk*olsDc0O)2CE zn&TBd+o>EKvzISBZy0)FHT0(|sdwfZ)+ggcAs{Z2{|#PjAWaAYlO zy;g?O+Xegl{_BAc3N^9ipYaqinDfAlQ<`mY<(ZUB5j4=2sdOf^nVRosZ^i|1OS&S2fx!QE8 z+!7W}-EXB8$T53V0 z3cwyrO!+~l{bM)uwUtLy_V9+(mfF$^rf{K{IEGo|z$ zGywcU^qeH@@kfyBRqG523W^r?aP}ii{U4N(@RS!sBtWUz9r(8&eulY9$S^fTvFg4m z(G>%Jj2J;lh!z?)d@)ty21KdAa*!Dac*=Vd7!%VW*Irm#`vHapIky=;?7^M_7^eJ3 zNl6J%m715}m`x%uN&8@lEm9aN04E{kjaMl?ZCs#&T;K20C?5BE`o$;>E5qur%PzJg zTfebDavo97d|&r?aWhY0+$HVjF_Md_a9+MZmZi~J6KLwkHH^)sk{N^Dl149h+B0oA zJp5I`Lt|64#ZZ?IV?q*FfLHW|#d9eKx~7 zdFWN@UeseyW}#YNqM@Myo~K-B`~=dmMP9v6$JbTU>xSb~k`)u?+6?}r?m+C>H@Nol zpZ0;A<7&S9XHkZT){lw3dJ<>h&t%1E9H1tYY({Qf(Sqc;3@$G6IVL(Z;n8dda$CZs;(>tB8=8!CGCZQ|CujLDEppb-2C3{%&t2?ED3-B z7LbEdq~m3^*UA7Ked6Z+^^eaYGBcTWxt!xgAPvy7xTlt7fW720wE$BIcdUP}bs9YM z=t!Rw`I4jmq=w&W!OV^bmNbjtk0T8FB&#uJYL|<=T9G|=+VkSB5hdK1Fq{14kj1sY zGOh}C@i4n#vgZP`jvztv>u!ITuf!{lL2QT9BQ8%E|F1AXk+=c`7|i~riwka4`@;wY zCg+|ndf!aqxa6l?Vs{+jqkdi}Dnd zWsSaQ<5}N>A1?(kHJ6L8?4jEUJ7(qrk}VLFFgfnY#vH5?5o&Ce)dTr0bsE7CE=M zccuO$F#oa_{;Rq=tWB4*k5efvnKl;<>K$*3)6%ZJH1o{u*?aW?GBZi`K{2-mhXFe1 z2_wO#st98dxB@7cA^Y8jaJ*&kj7KhK;4l+;QHp)N0_p3qv6v#$&K02u{I4N%d$2ka z)m|TtjrapXf|{#ps9-?K_$W#{YQZGM(2n*uR{$>V$Z$@O>1d5`eTt&j7P=6lP0n53 z$IM8W`o}~RF@>j}URS?Z2QfBBVD!knkL-f-2(hT(J>}L=QawCjVdfFdlsC2Ro9x_P zoq^>P{6ELe1~Yp`nj!!Lsbm|7=dPD$$jZt$W%wvZt$u|(EN{fU@V~jchurHTf0!g5 z@6C>?4o;tDbI}L$3j1{CrihqULwQSxE_qD>R9nd>d` z$LWgkMSAR*FxcM4WbDC#z=`vD=D#0+(U#WcX8Q3;f0*)5VxXX)xa96$lPT z7X2`%tj*;x|Hz~^oUl3$)EkUq70yD=5B-6X@A4UmfWneW>&&5^b zchASFHxFX+&tK2w$HprB@FAj;@FTt}K7eoBoTYpsn|mu)UtdQp(2}&2tW-8rd5qbx z^n+Gud&V{Hu#Yz%r{%;RAJ5rWKJmz(sAxGpbe-Rl-Vk2$oJ#bl-1O)^^z8oC&t@G^ zZ{I#ZgS(|ius+eEDLkF}xc12N{1jplp-Id%3E|*TfT)lZXJR3;Iu;y$DjtQ^86F|= z4r?%+>WMNIdARKp`U~oj;hj$;izlAoVPVaOFG=ztPa}!gF>VcPPt*N`N&G~f@FUjo z&q8!tF&(OzT0%~DDL%2&Kqv}iW;mjOZaCyw#K~VKAw>V>OPXGdy{`<5cXMr3d8RmV zJ|}l-SRHNatlH1WEfPr>4gU_^J3Z%Gvqg`EHjP&EL zM$xo4*%Z0cs}?Iyn<(Uykf(oseGbs8z10YtBB(Eoj(`TQ0YuZ$IxmXTx?3U9Di$kt zO%&Zpfh3!|w;xHj2|zS!XENd6&&3H&E2lW?XB}St4C6&8^Jx!MWtN^sj#lVq*>1vI zh36p`u3NQg>7c=(+9R+J@@q5SSVo8eWL7@1>sA4fk0#Fft}0R5UyC9-w*f?&qgNg5rh?QY z1Sr>3*)THnvh*eba94&1`jWL|UKa895n~gRd-_wO14cMo|9G@=Y3XRAF>&`&Oc<0_ zPEqDtSwVbeBhhRo9$DEO%v2m&if=v$e<_oUh^v36yTxzw1H5eoDnD1nmy2D-{T-(B znQB_&BgQsHj}v5@{~8W)yx4t_MxaE>Aw&6{4pgQJkNv#NM%U%ksp`(!uj7(0H#gRG zpMOnV%nq7}qx9DQs$G?A@L}lwv_~A-vEqzIt`O?D+~%$@J^ggyb<#-YPd+0zHzsP5 z5SRG*2Qp&`gY}V>k#-83e4F%>cZK(+r$2bel(%)%#+VY_1wHl0DX9Cp~I;TsxGdSXj3SJE%zkr#S6Ch)7bCMPqZt% zL_xe=o8A_Uk|$T{3z9(TIC~`m?B?)51{hKD+LgC&xG2@meyF#{CB^%}df$9#`MhN3 zmQ!r`;`ixL)K`XSla{t$CtfEUvZ;ciepHQp9q(C|x;cuZ7#|heuQn7;qyBUzSDsWo z7eVz8+a&8f8BV|LvEMSS#p%F>+Ax^%6xs3k(Y1Gcv$Lz+Xwap9w8E2fR9hPrfn?5z z@t%)4o|-eIcB-SEY58BLYiaE(7<^Enxo_65)LG%kfXDKZ;#TSUqiOb5V{4OaYnSQ8 zAj}ZgcmOVnl*K~n?M#^`$M(wzSp};(k+t^2HoL6Di%&%>SXZxmw=NW?V`Z$|Xej8l zh;Yiwu-?g!*VW$b(z0RHgNRI=(yNxPKX@4_!)|prknMME&xsQbGQ91XqRB+c6e&wQ zXDk!=fqwhX#4l(LZy>sNnUXyj}r?;@v>!C?^Y`di)1>@wNr-SUMV=hxHfkIJv-TkIBj`hom zDP>gcs+k8Cf7J#odCg?$--e}9EkcrAd1`2+#hp0AX-IcV&TJcE*8J`z!+1Z^eh;tpr@m(asn8h`H_ zjL7^_$MK_g&Hoe`CRMG(z$<|F+=s3C8=c&i?|orOBKv{#g?zk@-z}`Xr2>nI>F+KSt_g;lo7DZKcej1%WzS{x8~B zOS247oIhVwdaL(8F#lOe!CEa@M`$kcn=SD{V>F4>4q3jCQf{pT=^aK!e|Q&vGKW9D zPq0;AufIF4=+)qKE$m8-2B`M{i657o6nB#CfC1I?4;nGS}Scc(d zN}5>q;dM&nw+Rj(vK4ftJh`j1Mo6oaIy?V%O6pncUl;);p1nO8zNe&z@N35CYV~I5 zOQHHy!nB25E+x9GHXQGHX`eTWZEp`iby^-kZ)GGa+(HdvuRyaxNx%E>li~TR<;1H= zZ(U4mGLBG zKeCGKm32gkko_bhBiSqaSbY_uIF#8T6xkKo;*gMJuN;(_LpV70_WPXY=f7U3j_Pwc#pNH6miwSjQAE9)Qajd-DTZ*r~yx1>lQH=E@u74@!^U| z7I%IN-u(%bS7{mP-DFr0CY9nkHqXd zYuAEmACt?tD8`C2cV;a##?rZDTWu-=wE z^0s#)+s21Gw?=TPdwfW_A{d5Gwb#;_za#0Lx1{|;irAQhYLDyx7*jmY302o-Ug?|t-~ zj~lkO3Q+EaC>%P&in)_tcn#%rsm;~m<94r$ei5DfV4;h@zlD?s^Dq`m!9ly1;O^_( z^In%$R@CY-k5J(4oagEG7ni<$;B3D8iNf*8I# zX;0w{>A}>&I*E6`-j&BD?MFg-dJe9m(lDuTz+>3Ve5~prZAQBU$biAMN%2c%mtDi$$ERsNO1#TZ46_YF z{0+*{G(_CFQ&Lj%(k@W#0PoHjDSs1xv@|jO+e}`4{Cp81+T{S$uN*z9Er#L79%n@-)1c?}r*FmNq zYE5pr6*p!-iQ6POY{(uK%nI^WB;bo?b!TCg$IY9Xj;?HMwU;w?uw-4hg4BD^`@8>o zTa7#3q?V^Fv16h9nvNJm|5?ZB!!5RoV`1l|kb+8`Et|}K@8*;{qs$i+rHs;~m0Y&P zZGUl(jD_)xY;j$MkDX?vVK#-!-Z?=sI^$mS`r8d%wezM6vToHL?wwI>Wf`@!FnD_n zCKLUH^gM%3WVhsYwf7?r|9SsPIVrOt^Zm}j#HD9R()(B7!H9Z_($@8p1v*gH`5o2F zjo{02A6gQ8k5F`W-(gqq@2pnd!_N75h9IVfCm0&j>c{Y65>4M>Z(U{b5$NVlNeals zN3&>Z*F=`3%`&#_1;E$HtzudBs)eY$ zD*I?E7j|>va_)+fM2e`1YHeK<3RY?>=m$*rvOA`uj7l_v}IHV9WQB8?_HIY9gp`s2%WxRDG3))P&r_J^H_{3Vq#I`0w9! z(w@SGYhY5ZbL~U}lj$T>pB;^Pjnm~h{FRey}K%Afjj4}cc?#0nRpmT9YKt# zx-__O<4IvSI9@+rg0}=T7KKsdDl%g965CsDYm(}>c*>EvQ|i9wXSvSxinPPp9(LWs z4W&r18Ogg-pwll})>V&fkYl9Jn1sn)S^$q4!zKkRQ8Fcw{j~~|&C=4+xt}#<2BR_p z!*NYMKqTzPw|izbL2{bRp+0@OYjykxh#|;qwGCMzNGEJ%}oFB&DQS75y&5CwD-&gyRdPJgynyDmDfeoDAHqz!~>~?+&GP`DPVD;36SB%Z;4r z^gxKm!6_Mn|IWe5Nv1?V8tuK*4|oGLJy!=P>H*L;Mm9YL2b175pJ!xzFxmvEyl!u) z*0|94DMbH)zi?T(_1x1lH78zKIrC?#(HngzbMgTQH3&WTH44Xi}Td(9`mCyQuby-ERy24T}}nn)$uT-5w~&+`n& zmFHmdfgAW4?h_3i9RZSqS#Z}zB%zq>9`H1Ooj3v0^eyl_KUP%O{r>iXoKQCx1-bwV zI7*>1itHppb`NeC83*e~MK!rg z-R;8#`GchW5 z&#JeqRpmWAJYe@M4_yG@afI4$e}O82n3ywg6xV>xJX}-q_VRKZ`oIZTbZn;ue9Qe>JORJiRM5Wx1Ll{P$HL=E$Boi~iVdjn91x(T08V!d zISL8u>i<8RSq2>G?j|uBhf{`8V*?x_T>7cyWj#2q(8gIxahosUd30f{eHlE<5fWga zYY1}jG#53!W$_tBMn)w47Vkh(0RuUVe+!M@0LBeZaerNacVgUo{2u}#_ux5_5BLBD z!0}Jvw!?`yIXU_K$*N5xHPwV^r-(Xn3;$bq?nj*4X&T4nHcf5qX%K-zbB9QJ^l>gH zVHSVzK%87K3C#jw@YKVVIYkcqz?eCS(9;V_Nc6s5Bcp8N<44%m!7X2j-#Bq<2L}g0 zN?ZnHF*z_4Tt_R3*lv|z5zt%VD{^3v_$=aIs6|LCR(7V#Ek`t83NLsW>=g`WPxj4W zj97yY300votX9EyHAvn1e#`oXP(XDqJudh9HJjUz1Ht7DFQh_Sq%Uw6OJ zvc5Q)XSpS<83B<2ni9bpgK*XfpzFSZKR94RV`C^l^jsXnrA@U&gB!_4w2h684c^4s zXU?>yyuE@PDzS)K^b|C>4ohB@dSwh9ATX!n>q8Xf<>eJ-$hjcWREvDzdj8nQot+&( zf*XOX^yTXA?6d>rE3hhC|02}8@+zL);p{;3BLwY81s;8x6{m) zG0K|>GFAhUW9uxCMBm?8tFS|DPaKz&l-%B5>xKjX=WAByxzQ@Ip3HK83W`%`aTto31ZggCGC=c!XnSi05Zj}%jwR_gP0z;G2a6Bq+?t+56Jn4tIhjIlDNJ0P{)XmJ;AXt(pmf7IO0(ec-vu9b5${VSPn|*V; zs0K~C09OOL6tIA~Li9ui)(4vdtN(rfUh$UNzMQQqr!aZS2;mS@pV}6S6Qm;rqSLpg zpEvVZ+86KBhE8?z?|Ggi%?I*BO&3_21ksiHI!_Xo0cY@t2L&*joSHRSslP;-?CrNt$Tn7~@){Rf#B;wpo?>Mu zRK4PUq-vb+S~qhT@euS|e2sTr@76SgzEO1ULyH%OtMBIJp~mywPG}{GMB=8isk9T8 z`98vqLpSq^Rv&`74$7fZKaNFaL3SY7RNO{de%RE9OFW!bMU>E!IeMpbO^jLZ6px@{ z8t61^1D5tT6c$oM`CR*#$=XYsVJv#6pf%Khu|(RMvR`gkSVCUZQYC0}@r`3|RxaJO z^Dl%<%9uwA<@@IjhA)1v{RA5hVeu%lN2((YymZ{i{nZuvkDx$+ZuYIMYwGi6XUtK) z+%$9X+rX=wFg4W59X^THU_uc`V%N)!=Vkj{Zn|`5)1D*&j|i;xozn%Rb#1(y4qT z%;f#ZfE#m61u;8k*26N+VYP%ZvB((PW2fTM@JB9aOPRWgcB*^BrGQI6a=V=cX51Nz zZJJ#~CiaG#qT#o$*wY4o|Gw`qZTV~T^`_rpWwQ6A;Q5C8OsEZM+;fojCz~d&PKn;V zI;SDf8o_X5a%!pVi`yRBW98(}q9AWXQR0!xP_Z51Til=Fp16wB1jlyKQ=Hm2%-BHn z`*ViAraI`JjnmD0uk4Q<1RGzW5K-{Y_N}r!S6X)b9|glQ?HmnS?y!;Q*ef)jYgQP4 zE8{oLT6ZmvvesJH^jakhC9OkA$&G3KuTQtA<0f#8yr9y|F57D0YXgqkAg&Wgp6R$@ zr&#Ff2X~I~+UPIp(er!>kplq%=R^xl8;HU8uK+dWeht-o$r~j^-UKEU1-|gwiX9{3 zewFjXH&}1{WOY5*5hP0{h&4k+nsn=wimWbv$Lfw=d9M6pvvR<3fC~JCIO1;8n3b-IdM+Jz%Z`^ zbX=k8Pd0AmQM^HZ^S><9b45%`rzv1BL-@ADxj44XPJT|np0Piuq)JnZ!0hzZRg!zV zSNWG#sp?t#;%+Z*{LU`Utk-^9QDI+1fV=-xzId1hfz1P5`C?(MvZ~*0W_eRn{K)W& z^dxnibyl{@A2>Z;Wtk)w6Vbm1RLk~5zK6+l9~R*{VtnAiB=3S|qh73YNW?Fg6pJ6M zy@4a13O4>r#(AM(s54DxSw8kL9?VbDZgt=!$NY()8Kou5J~!E7Q`Ly?OX$Hs^V&=u zWHp^H-K$`0sjopjN4EGxsM3rdZHOvv1s@z>S_}?Fpj~JMQ}( zwCkt^b4!qTE-oy_XZ9o6bLm%LDfpZEDo-<`QsGeZaA#OU94C*DrV5?=xQ@=csqP~E z^z5?3nvHhnp!PL^;L}%e%R{qS7u(RckFuo73uFbyYhiJG7AC&S}~vhS7eA5_LL|n_Q;>ldHwaagcNS6 zPu)k9uZ>gtOF9+n#em7C1?)D@H6d*;Vw6+7qW;a6$icJoX(9lpsWUtT} za4^UQF-Lo+WJv zRx&^m7@;;jqv-pw*P@Pz`+Q@#K*#+XW*G7TP>|I$W&$(0P-ol?CgUs$X}6I?VChS( z_f0@D%th!l>L%g*Y@13WV%S;kCDIPSur;^}P-!FFG<@CexY&7ytbbtp#H@236J2&1 zy2~WB(!ig&_z%$HF|5j|poIN1KgVMf$vdVGmfdeKQJ6(7LL(v~3`T%=zfQ>pcsDH8 z9Qt|=Euw2&$=;~_GJ*CTRxSbAfp#>AQfF5{iB8n9xB3_F98SlfKh>_y?4_3r*Pk-z zsr+!aEk^8#6l_(=M(@CXM(=0fOt;s%)BoDuA=$<0E{=Z!pZn?19dHPtN%1B3D!)}6 zYZGi-F%Qqw07=1eJ;zW2h;s6l@%;_eGv{S8`euY)Dj=6hY&&~+8)if*xw@TY$ENM< zCrBeQM84RAJ)r?)FkzQ$4{#ZuRh)zM!q>}bY~L~4cZ?zhEIl1`xorDNC}V;f7`d44 zMm>wUod1?m6K0%*Z?J|2xJf5X784%+*U!R0gZH;NU;R=p5x&i#tu1U6dC~G7Ja#of zdx$_v*Ws<^#t=Ts=+@TONT3|xzYVbHiL~MayOi-eEFl4qC3k%_X~dGr!P2r)9nNGl z)j1KfGh$+5$Bwi2)rSPZlGbwI4LIPW(i-Flg~ejw)nC_qibd45JV-L5>B2CjepCvO r=(Ny<4a+9yu1tr{M=O6z5YSX`tkn&MfRxg diff --git a/hamilton/async_driver.py b/hamilton/async_driver.py index 63d3a8bf6..1c79ce6e3 100644 --- a/hamilton/async_driver.py +++ b/hamilton/async_driver.py @@ -5,7 +5,7 @@ import time import typing import uuid -from types import ModuleType +from types import FunctionType, ModuleType from typing import Any, Dict, Optional, Tuple import hamilton.lifecycle.base as lifecycle_base @@ -199,6 +199,7 @@ def __init__( result_builder: Optional[base.ResultMixin] = None, adapters: typing.List[lifecycle.LifecycleAdapter] = None, allow_module_overrides: bool = False, + functions: typing.List[FunctionType] = None, ): """Instantiates an asynchronous driver. @@ -249,6 +250,7 @@ def __init__( *async_adapters, # note async adapters will not be called during synchronous execution -- this is for access later ], allow_module_overrides=allow_module_overrides, + functions=functions, ) self.initialized = False diff --git a/hamilton/driver.py b/hamilton/driver.py index 868764e58..0646673dc 100644 --- a/hamilton/driver.py +++ b/hamilton/driver.py @@ -13,7 +13,7 @@ import typing import uuid from datetime import datetime -from types import ModuleType +from types import FunctionType, ModuleType from typing import ( Any, Callable, @@ -402,6 +402,7 @@ def __init__( self, config: Dict[str, Any], *modules: ModuleType, + functions: List[FunctionType] = None, adapter: Optional[ Union[lifecycle_base.LifecycleAdapter, List[lifecycle_base.LifecycleAdapter]] ] = None, @@ -435,13 +436,15 @@ def __init__( if adapter.does_hook("pre_do_anything", is_async=False): adapter.call_all_lifecycle_hooks_sync("pre_do_anything") error = None + self.graph_functions = functions if functions is not None else [] self.graph_modules = modules try: - self.graph = graph.FunctionGraph.from_modules( - *modules, + self.graph = graph.FunctionGraph.compile( + modules=list(modules), + functions=functions if functions is not None else [], config=config, adapter=adapter, - allow_module_overrides=allow_module_overrides, + allow_node_overrides=allow_module_overrides, ) if _materializers: materializer_factories, extractor_factories = self._process_materializers( @@ -1866,6 +1869,7 @@ def __init__(self): # common fields self.config = {} self.modules = [] + self.functions = [] self.materializers = [] # Allow later modules to override nodes of the same name @@ -1927,6 +1931,17 @@ def with_modules(self, *modules: ModuleType) -> "Builder": self.modules.extend(modules) return self + def with_functions(self, *functions: FunctionType) -> "Builder": + """Adds the specified functions to the list. + This can be called multiple times. If you have allow_module_overrides + set this will enabl overwriting modules or previously added functions. + + :param functions: + :return: self + """ + self.functions.extend(functions) + return self + def with_adapter(self, adapter: base.HamiltonGraphAdapter) -> "Builder": """Sets the adapter to use. @@ -2168,6 +2183,7 @@ def build(self) -> Driver: _graph_executor=graph_executor, _use_legacy_adapter=False, allow_module_overrides=self._allow_module_overrides, + functions=self.functions, ) def copy(self) -> "Builder": diff --git a/hamilton/graph.py b/hamilton/graph.py index 43ccd24ca..68ae94d5f 100644 --- a/hamilton/graph.py +++ b/hamilton/graph.py @@ -13,7 +13,7 @@ import pathlib import uuid from enum import Enum -from types import ModuleType +from types import FunctionType, ModuleType from typing import Any, Callable, Collection, Dict, FrozenSet, List, Optional, Set, Tuple, Type import hamilton.lifecycle.base as lifecycle_base @@ -142,17 +142,18 @@ def update_dependencies( return nodes -def create_function_graph( +def compile_to_nodes( *functions: List[Tuple[str, Callable]], config: Dict[str, Any], adapter: lifecycle_base.LifecycleAdapterSet = None, fg: Optional["FunctionGraph"] = None, - allow_module_overrides: bool = False, + allow_node_level_overrides: bool = False, ) -> Dict[str, node.Node]: """Creates a graph of all available functions & their dependencies. :param modules: A set of modules over which one wants to compute the function graph :param config: Dictionary that we will inspect to get values from in building the function graph. :param adapter: The adapter that adapts our node type checking based on the context. + :param allow_node_level_overrides: Whether or not to allow node names to override each other :return: list of nodes in the graph. If it needs to be more complicated, we'll return an actual networkx graph and get all the rest of the logic for free """ @@ -170,7 +171,7 @@ def create_function_graph( for n in fm_base.resolve_nodes(f, config): if n.name in config: continue # This makes sure we overwrite things if they're in the config... - if n.name in nodes and not allow_module_overrides: + if n.name in nodes and not allow_node_level_overrides: raise ValueError( f"Cannot define function {n.name} more than once." f" Already defined by function {f}" @@ -713,13 +714,42 @@ def __init__( self.nodes = nodes self.adapter = adapter + @staticmethod + def compile( + modules: List[ModuleType], + functions: List[FunctionType], + config: Dict[str, Any], + adapter: lifecycle_base.LifecycleAdapterSet = None, + allow_node_overrides: bool = False, + ) -> "FunctionGraph": + """Base level static function for compiling a function graph. Note + that this can both use functions (E.G. passing them directly) and modules + (passing them in and crawling. + + :param modules: Modules to use + :param functions: Functions to use + :param config: Config to use for setting up the DAG + :param adapter: Adapter to use for node resolution + :param allow_node_overrides: Whether or not to allow node level overrides. + :return: The compiled function graph + """ + module_functions = sum([find_functions(module) for module in modules], []) + nodes = compile_to_nodes( + *module_functions, + *functions, + config=config, + adapter=adapter, + allow_node_level_overrides=allow_node_overrides, + ) + return FunctionGraph(nodes, config, adapter) + @staticmethod def from_modules( *modules: ModuleType, config: Dict[str, Any], adapter: lifecycle_base.LifecycleAdapterSet = None, allow_module_overrides: bool = False, - ): + ) -> "FunctionGraph": """Initializes a function graph from the specified modules. Note that this was the old way we constructed FunctionGraph -- this is not a public-facing API, so we replaced it with a constructor that takes in nodes directly. If you hacked in something using @@ -732,28 +762,28 @@ def from_modules( :return: a function graph. """ - functions = sum([find_functions(module) for module in modules], []) - return FunctionGraph.from_functions( - *functions, + return FunctionGraph.compile( + modules=modules, + functions=[], config=config, adapter=adapter, - allow_module_overrides=allow_module_overrides, + allow_node_overrides=allow_module_overrides, ) @staticmethod def from_functions( - *functions, + *functions: FunctionType, config: Dict[str, Any], adapter: lifecycle_base.LifecycleAdapterSet = None, allow_module_overrides: bool = False, ) -> "FunctionGraph": - nodes = create_function_graph( - *functions, + return FunctionGraph.compile( + modules=[], + functions=functions, config=config, adapter=adapter, - allow_module_overrides=allow_module_overrides, + allow_node_overrides=allow_module_overrides, ) - return FunctionGraph(nodes, config, adapter) def with_nodes(self, nodes: Dict[str, Node]) -> "FunctionGraph": """Creates a new function graph with the additional specified nodes. diff --git a/pyproject.toml b/pyproject.toml index 24f8da0e9..e7945e15d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,8 +57,7 @@ docs = [ "diskcache", # required for all the plugins "dlt", - # furo -- install from main for now until the next release is out: - "furo @ git+https://github.com/pradyunsg/furo@main", + "furo", "gitpython", # Required for parsing git info for generation of data-adapter docs "grpcio-status", "lightgbm",