From 9849ef5dadd032f8d65707493eae834c9c156903 Mon Sep 17 00:00:00 2001 From: Gautam-7-7 <140533905+Gautam-7-7@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:35:18 +0530 Subject: [PATCH 1/4] feat(di): resolve dependency injection gaps with auto-resolution, thread safety, and cycle detection --- .gitignore | 3 + .../calculator_server.cpython-312.pyc | Bin 10920 -> 0 bytes nitrostack.log | 65 ------- .../__pycache__/__init__.cpython-312.pyc | Bin 2119 -> 0 bytes nitrostack/core/di.py | 177 +++++++++++++----- nitrostack/core/errors.py | 4 + .../test_basic.cpython-312-pytest-9.0.2.pyc | Bin 11792 -> 0 bytes ..._initial_tool.cpython-312-pytest-9.0.2.pyc | Bin 4757 -> 0 bytes .../test_oauth.cpython-312-pytest-9.0.2.pyc | Bin 9410 -> 0 bytes ...st_production.cpython-312-pytest-9.0.2.pyc | Bin 18648 -> 0 bytes .../test_tasks.cpython-312-pytest-9.0.2.pyc | Bin 16390 -> 0 bytes ...dget_metadata.cpython-312-pytest-9.0.2.pyc | Bin 6602 -> 0 bytes tests/test_di.py | 129 +++++++++++++ 13 files changed, 264 insertions(+), 114 deletions(-) create mode 100644 .gitignore delete mode 100644 examples/__pycache__/calculator_server.cpython-312.pyc delete mode 100644 nitrostack.log delete mode 100644 nitrostack/__pycache__/__init__.cpython-312.pyc delete mode 100644 tests/__pycache__/test_basic.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_initial_tool.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_oauth.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_production.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_tasks.cpython-312-pytest-9.0.2.pyc delete mode 100644 tests/__pycache__/test_widget_metadata.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/test_di.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0bc6ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +nitrostack.log diff --git a/examples/__pycache__/calculator_server.cpython-312.pyc b/examples/__pycache__/calculator_server.cpython-312.pyc deleted file mode 100644 index f0de6648f3872011385f996a793af97768d2a3ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10920 zcmcgSTWlLwb~EJ6@F7y99@N8@>DZE-_^CK?B-!yg55=+UIHsL&Db7e5nXk+Y zZEGSe94Bq$?Y5E8qEWC(<1NsxgSe;y6ma%yZU3wn{n%loK=dTQ253H-pMtUsWOv(- zo^yvoj$|g?KF|Sp@11k+x%bXF=bqQ)Kl}Y&3Lf9a@Z_O3iuxCPu^!R`FRTCNps05# zffDEh6)|f%LX&q#!~wM<;Y>0SCdo$FBp2b5u851qan6K0>4|uf-iVi`M8{-IfxpZ&89v^nC0zSBdZh>ISIC2HotS#E{rFPT%la`yk`0V4Tm|(Z}W#RHU7ZZhvUB zA4XR{G`a&uHwXc-W1JQN2Y)w$A2a4;WR)7gt9=Nsli<~fShr3-Q(W9u3H3rqXb>9T zcid>Qu*V%YnyogOdjrh9(Snq|U<=IIC}PbHd!r#%>(vk3sS=F7TVvU zB3s3<&~a`nMafMt4>fthwzXmt;9D&$i>@>^MNiS;&TnwcaF{V%&q=B%#}Wo}NQ$e5 zb09TkFwaSf8g>|-!I&aGpBBV~!9FdC2|;u8^n|MoMop&^hFcbubXJaw2A7f3$&6}v zrPQPtS7W0IfF;vH7HU^Ao{7dX86)(3JTs8VB&2vumC~uFV{tVtPZ_=uQ9duqL+R9* zG;RbBUlik6><54x)N-=7y1B{nV^J`)i(iHrHoH=I&}efcKD zSVEnM#wW!1xwxG`MR}xFCjj~`&=1dh#Bma&R&)yV4TnXq2qUs0CpZtlIMT=1`dp-s zE%kA>KDX!*TzL2a+l?cpJYlaJQ3gAfNXJwo5RJxCskBO#SE5n51xK`Ff%N@|T7{DO!{AW=*^`PWD`&@JSv7Vx zC8=^+QDgCQJ2F%1L^`!Y5zd_zFUFFYgs7a27b%pMqksgW(UX~yn{ZMra0%scDE@@H z7pTA1wiM{QG;k@q$n@dsrlr76^L6LF!6W(l{STauA)32%boPSY_+&0{@B!suIXP_2 zR#_%e(EY!V3Aex&`OcLj!)22Uk60d2mW&{XE_|$dLQG~%*=WHgGB+!W{P|cSD~h>=fuQ$DP?%Z;A};4 zq}F&MCXb8iLsJ+oHBClA)f#-&z$BrK!Iy!Rnh2d|| z)`b~)4^)Oz0u|~6MjM`tEXE}T6bC4JMiCQZWQ2kr!1FS)3$=m-!V!&X8y@=N9;*+j zub@0!qVf$bv&>JbzHV-rKk?z2_s)D6eJ`r-c;eI77F!1vn+Io^Kd&}9hFXMZ3&WAl z7@RDEjHlv7qYQl%g>6E+JJ5KSng*pzj2#D5bZ7vZVNwSH>aan*1iA~LP8$^H2xG$l zWeBuk+99x{RizebNP(kbDg4VcS)qzxRoh0mrky{fJ}4Q6%e0My4Distd)Oa77OzIA zuzOer|C5r@9g}%1He-R*)`EvXB_k*J(J5XN<+N6NGIcJMzL2tMcR0)5mrw~DmgTf; zuyQ(^5@b{`VOD+u=PD6ZJ_xUd6U?)MLd4@^GSp)!S5e#eFh@(Bc!-tPp#YXv`R77E zemq}WHy69wb!8~;56ty{=5PIez`h^9nhyl$27Y}0%LZQG{Mcf{EbhZe+gm!=Cqg76W139X168wTr=-Y6{(sXtuJH{5^~VDRC5~ z#5qj^&ymxneE6nm!Ks0K2#f|K5m?RSFAH!E;WJE0!kp903=6oHZ26`c7>`&CV!=GH z7K~s4&YA79luWZA33eR-WAU*`f@@gr0f^iS#Wy?_$fR#PP>he^Q~TOeVTU{f?K{}j%Uh2j$RRaMR0>CdX# z^1;xRfxN$NZu&ESN4}S+!XFv0AE;4z3^VRVD=+&MpLtlaDj9uL}JM?u$77vF#>+MPiHYB5QAa=!YDyl3W z#@FYuSZ7jpBMb}FSjurI-l7VOlO3YxXI3e!3tm6FZGLxwg6i5yQY|=1^(niGz(QLM z^mG*{s21u-_303)ZXJGrKvNcRkvRNH53&rO)xQBfQrWzK%g73#dC+fn9E}VFU@rMhs*A%az(}cr}1mD{K-VAQbC_%|g4- zaih};6Cp(Fvf9EHvA(!=NNgz5soOSFBh1hwY_;?^(6kwhd)PK&!_>C$BSwAsD1fU4 zE@&bl%7zb>9&t1wU|3_l@e~saR0|EzEo-bdSXz2GOv^7q9~@Sqf3a;hpG1VU)=JBk zjOQuD0sVuTwq@%q^SQg(6gHH1!(-u!+L2?XYtN6y6!7|O0RVmgMigK_6n-nX{o8rV z?cdH@ZvS@Pbo;{)@iFMn8>}j+K!6Rnj!(j1B&tB1l#CfHc>~YUjKJilU`QM(VLj+p zy$Qe7mqG5IrD&pgC#_yvJ!!`(!UeFem2BltA!Y;Oi4mnfqUN( zcbtY4(hTz>W~5kOpf6Atou{b_^glQ+&|%lG)>w>&Q{%ji%l*7T^M*^w#^Ye&!FvP$ zNrCXr;2@|{!PV!+Vp2jBpobOFS&(0WO@uuLmq?F~gUmBh3Zg{fF0@0B8MdMH5-n?Z z4quFmBm$L(5wYlAps30(V{sM=gBI607X;aEaLT8w$J)Jk22hp1g5qDP-(8{#)s!dr zPW6@QW%tIMd!ydAdqKQ8{>k{l=}%*~*k1*12lRu-^kXmT$4@OEKb<>%T7NO3x13pW zpDj2T&;ERdS@t7D6{0KWY55>zCcX=)Sf4R|jt&TnvrOKAmstT=3-T z`gLFb@4gH)-KS{J{x4hkYc)T8>H5W7-M-I#`~JP)1mJ%t=ptVpa1L&xZ*vC+IqD8e zL;DWr8SHl63HS%wop)MjY`3xqZ)dUHjpL0vyV)%UZCV|W0E0E&7RhOQE#UfS4Nh+{ zSliB*gSG9v8LWjXAHnwjuOkR9Ri(qR_6YtT9KZ?+pz_N016X;4c__(YVu!V+BHbl8 zO*Yc^^BTvagUM?Q#@C?DiF`98fOg401euUySP%znC$>mvOAZWrBoJE>{lE5rC=7I= zV@5o0S%5cftEJD=nC~i@pq{wRAuCD_DpTT@RX!$r7P2>A|K#t6k6 zM320xM}_5RA{R~QuO{`D)RH@$uL&*JwC8Htmuq@+HN6Y$&8knTK0W!%=r5vo#or`< zom{LrJIm!a@YlQkamPnHa)Euid*APgPkDLJKNz63M(gON<^JB@QqD-xLgjDaU$Ame ziq?&(Lq0tQHYk%x%c>~YZIul_2suG37uY+gKo|;1tE?=^n8-BEk|!xaN=lu|tToJN zREYU$fre>*#vynAw-6i)&T+>YuFI`BTPZREl~`ZeShTzw!<&i8U=<+~st^bBqoPem zeP_4nMm5CFHbkwXOa>cE*A6<5li)@OH}40ju3 zl8}BLJQT0F=YD?Iqdk4z5n{FaA+v=EK#&;1fw0p&RhWz;W=FxDSm(*8K@t~tERZkP zwtOcm){H~ZQ81Ngo~yPVuCiH-$yQLx{Tpn7hxgZI!4hX)Lozw!S2#7yi zOjm0CCy(;xFy8c^cxx<=gn*QrAj+t7Eb;@;9Ivc#H&I>S)>hFYv*?TxEifR{?E4n2 zxnUz%{!nu_kotD#*^GB zC9~wm;m!1S;yZ*7~((Frf!YFKItOqIAO%>jr|P6QG5Y3SZJ~(9_Mw z;5wmuw7bU=VK|Xb?Cjglk3O-RpTbKXh2OcmM|+0MWJMCi=`3!E3YjV?rLxfa{>fmj zqhu~C7AekEe3goG{Sn*R-PfZHkg3dIrZ|!LV(thMXkR`5C=2g#D_pa>TeJ~Id)4mZi?d2yGJ zwYjK*ypn2t>W7?Oi$!pKLJm}Ap94?tx{)a8$~Y9}>Ge_7weP%q<>k3^*LE-Z!?TXB z>R!n=3HhdYzTxzWhi&lAdJ8@(xZ#~2UHQ@csNVV1t=e0Ren{5amBoNM%Y5x?oT&`f-h zdlPgaWsGm-Bo-Mgrm%plBqd{(0_*_X;mSx@B1!UF*h0f?Fr%Oj|YG!mv0A zKjkiv0~2U88`it_tWr>~xGBz?_c!IMoAdRrp&~+N%W$D*ee}vmQC6 zzkEi2<%jyqKg^B9?qlDo-x|D9OXg|MS9j#=pU;Q7it{*Og5HDR1VQ~w#H^!nT^QG; zgf4(To4k*G#fhM`;-k3gyss%NxB#pU0to4I06dNZ_-6xPDK6b8n$poA7{;(dg!R$0SV+9wr^g*&GxN2DXx13 z0cIu%S!;O92Mod$w2Vd-sva{7rr>Q*8n1eNc-zH;53eq*?dOHs7((`?EcEaX{ zD{kupz?ZSe!Oz;3x3n$nS+j~00B^!?brjYs`I=OMnnB%KL626vj4WM+(cguIj9vFw z>1~=)t@L@tq@9X_Mjk6|_9mpsUWc9kwc}mqpRz190}0tbdaq7vmjBsaA-@o?_EWB{ zZ&|-H_>T>>m-@+Z__>IS#+cSe3m=8sR4JB_GzirD=dyR_rJ*%L;JSW^*}Db=Sb4Aio$!_LpFOh7gmO%% z;G(J?{T-N5w&4%tI{ETI!(cCcyNMorgt^^3;DeVtUG(4%=FS!l+dF6|@roMv9*shV z-xX7)QgJB_m&ywHP=kZy0npI=Ljp0Z#HONwG~Agf0emqjE~A)|KQkB%(IcFc0-~!h z->5gV-WQ9<8-$ zqU*60!UFBI=?4&VPaFhP-b7IvZiptMayqRN{X@7)7)CTLT=r&D>y(w@K4AWl=SlgW zU>4LY$~#a(u9K#}psK&1JYP`W-%^h(Qjh$W>H^Bs%-ila+;0co2+YxQkG{M2>fUz` zUOjm2(Vy@A+1{TY_}PI4$4%EKuA6~R0=L|YeMj%^TI@KsSoh)*b^KcC%H+@&R72-VC-?_iX~3?r?nTqiIip zf&%QX!%@IjFy4q#{gt~Z?{1y-ExTKD?pDZu<+nbv@KP>x-~kJ%m;%fBxd*HV=BO{t z5rWqOzTzC7d{zCus>b;<3*Adq{eT2R-KC)(crD=TeYcBlDAZDnZ&v%v*_>wrmyW#s W>>JNsW&V=;oM|jLDP{w)c>f8;hf}No diff --git a/nitrostack.log b/nitrostack.log deleted file mode 100644 index fbc40c6..0000000 --- a/nitrostack.log +++ /dev/null @@ -1,65 +0,0 @@ -2026-06-10 13:22:50,079 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:23:13,039 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:23:13,044 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 13:23:13,046 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 13:23:39,823 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:23:39,827 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 13:23:39,828 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 13:24:09,252 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:24:09,260 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 13:24:09,261 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 13:24:35,992 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:24:35,997 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 13:24:35,999 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 13:25:00,299 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:25:00,303 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 13:25:00,304 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 13:25:32,026 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:25:32,032 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 13:25:32,033 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 13:35:53,144 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:35:53,151 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 13:35:53,153 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 13:36:34,272 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 13:36:34,283 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 13:36:34,285 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 14:33:53,465 [INFO] (nitrostack): Executing calculation: 10.0 divide 15.0 -2026-06-10 14:34:00,907 [INFO] (nitrostack): Executing calculation: 10.0 add 15.0 -2026-06-10 14:34:03,731 [INFO] (nitrostack): Executing calculation: 10.0 add 15.0 -2026-06-10 14:37:22,534 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 14:37:22,538 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 14:37:22,540 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 14:41:33,833 [INFO] (nitrostack): Executing calculation: 10.0 add 20.0 -2026-06-10 14:48:06,730 [INFO] (nitrostack): Executing calculation: 10.0 add 20.0 -2026-06-10 14:52:34,751 [INFO] (nitrostack): Converting temperature: 35.0 from celsius to fahrenheit -2026-06-10 14:52:39,316 [INFO] (nitrostack): Converting temperature: 35.0 from celsius to celsius -2026-06-10 14:52:42,765 [INFO] (nitrostack): Converting temperature: 35.0 from celsius to kelvin -2026-06-10 15:07:19,747 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 15:07:19,752 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 15:07:19,753 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 15:20:07,388 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 15:20:07,393 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 15:20:07,395 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 15:39:46,729 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 15:39:46,738 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 15:39:46,740 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-10 15:45:21,942 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-10 15:45:21,947 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-10 15:45:21,948 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-12 11:55:23,249 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-12 11:55:23,259 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-12 11:55:23,262 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-12 13:05:52,734 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-12 13:05:52,756 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-12 13:05:52,760 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-12 13:32:04,654 [INFO] (nitrostack): Executing calculation: 10.0 add 20.0 -2026-06-17 12:46:21,443 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-17 12:46:21,452 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-17 12:46:21,454 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit -2026-06-18 12:25:18,921 [INFO] (nitrostack): Executing calculation: 10.0 multiply 20.0 -2026-06-18 12:25:24,581 [INFO] (nitrostack): Converting temperature: 35.0 from celsius to celsius -2026-06-18 12:25:28,765 [INFO] (nitrostack): Converting temperature: 35.0 from celsius to fahrenheit -2026-06-18 12:26:45,327 [INFO] (nitrostack): Converting temperature: 35.0 from celsius to kelvin -2026-06-18 13:27:32,721 [INFO] (nitrostack): Executing calculation: 12.5 add 3.5 -2026-06-18 13:27:32,729 [INFO] (nitrostack): Executing calculation: 10.0 divide 0.0 -2026-06-18 13:27:32,731 [INFO] (nitrostack): Converting temperature: 100.0 from celsius to fahrenheit diff --git a/nitrostack/__pycache__/__init__.cpython-312.pyc b/nitrostack/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index d4322cd1c9df9e64ba9cea25f4ea1508f48e2460..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2119 zcmbW2TW=dh6o7YqzuR%*T$?7%-A&?zQd*j(l|q}M7A2q(KpI6_ExS`^lR3Mq-Ek5> zBJm%1=Zzo1FW?`{D^I*pqRJ9aoil6K7kEJU;q$r8nejPu#=jMdIZb|ktvoqrSxx&L zmEI@T6>j!*P5Vi+HCtyI=tPGYi9wviAwd$5BuPk-6r@QSG9&|8l7$?}L7wDwToYpj zD3T(SND0cM3>8vQ_Bb1ZDyb?t!Ny^NOhAp)l%8ahFh!=6oMO{3LuO!>%)%U*Q?@j_ z1@mMcZj;-vKo($;EGm13-GL>t1j}R@R>%shl2uqEYp_n%VS{WyozzuamfeL-vI$#c z3%1F&vgKFeI*y!C-8tgfQRIv(o5`9ctjq-9@$fR znLUPmB1_iP?8?tNdD0%MSO36L*7G94J&%b@Ktrz;SX89^zyqI)g5x^eF`1$KsVm2B zQZC9TGWx)EJ#KQxb3-xyy1Q`DSo%ukI0%|8pe`51NI#-sXf~-RVUm~brDv%mqJaA0 zwFy3>VLc%#hu732`GXce`=Fed;YClWD#dgy${4i|gTM>wy38m;Uel%% z@}OOhiJaq}Q;VD5Gb-}WU*clZaVdtTz_VM7*5e`#mT#E8FKS1Yf8hJfiO%=Jw73_v zMd522T+%=;e&aOdF{{zByGK=9i}JT7b8PjqB15fLpJR`|@LFzkL@^#!WXM?0h+HeA zMzduGb|^|HgJau_UYP+E71SMB57{MO9w-!9wD^usLy<@Ejl-l@DhIyv1#N$hc|_*b z6_2Fcmsmw5%U_$cOzVt_Twro)Fb5pj@-u2Oer8C!bs?*k@mzymVgn0@_;o0Q{0-}g0A5Mzj{#Bu$pn3XdZcp*2fi-zR| zv|&^EWn#DgXA#8IXi6)Zse$TzIER=R4d!x=)agWZ)MY#UvWm%3FQ}`b$}Pr6opQ)V zdhpK#6h}Gyx88{9(R?@q4Cnbjc1^5~W-x7AzIL7~-@Ad54>wmV46IUf5jND9L8HOw zcYmnrrkuKl={v>+ZL4cBR1oDozvAkyh}ysw%HtvXS7@kCDnPbVK1cnlHPk2`4U)QU zB5fEZV}|jIb|bHw_V?pwd#B%o^1(Q5%1q{If9q|(&Cfh{JG3tbtfz*2ejIKX4Zkfi z`%#CV2G3(v8OdxTC=uGrOdduFWdRaP|p6##PPCoWSI`~W; List[Any]: + """Helper to inspect constructor signature and retrieve type-annotated dependencies.""" + if not hasattr(cls, "__init__") or cls.__init__ is object.__init__: + return [] + + # Get signature and type hints of the __init__ method + sig = inspect.signature(cls.__init__) + try: + # Resolve any forward references/strings in annotations relative to the class's module + type_hints = typing.get_type_hints(cls.__init__) + except Exception: + type_hints = {} + + deps = [] + for param_name, param in sig.parameters.items(): + if param_name == "self": + continue + # Skip *args and **kwargs + if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD): + continue + + # Use typing.get_type_hints if available, fallback to parameter.annotation + dep_type = type_hints.get(param_name, param.annotation) + if dep_type is inspect.Parameter.empty: + # If a dependency is missing annotations and has no default value, we cannot resolve it. + if param.default is inspect.Parameter.empty: + from nitrostack.core.errors import DependencyResolutionError + raise DependencyResolutionError( + f"Parameter '{param_name}' of class '{cls.__name__}' is missing type annotations and cannot be auto-resolved." + ) + else: + continue + + # If the parameter has a default value, we only resolve it if it is registered in the DIContainer. + # Otherwise, we skip it and let Python use the default value. + if param.default is not inspect.Parameter.empty: + container = DIContainer.get_instance() + if dep_type not in container._registry and dep_type not in container._instances: + continue + + deps.append(dep_type) + + return deps class DIContainer: _instance = None + _instance_lock = threading.Lock() @classmethod def get_instance(cls) -> "DIContainer": if cls._instance is None: - cls._instance = cls() + with cls._instance_lock: + if cls._instance is None: + cls._instance = cls() return cls._instance @classmethod def reset(cls) -> None: """Reset the singleton container (useful for testing).""" - cls._instance = None + with cls._instance_lock: + cls._instance = None def __init__(self): self._registry: Dict[Any, Type] = {} self._instances: Dict[Any, Any] = {} + self._lock = threading.RLock() - def register(self, cls: Type) -> None: + def register(self, cls: Type, token: Optional[Any] = None) -> None: """Register a provider class.""" - self._registry[cls] = cls + with self._lock: + reg_token = token if token is not None else cls + self._registry[reg_token] = cls + if token is not None: + self._registry[cls] = cls def register_value(self, token: Any, value: Any) -> None: """Register a constant value or instantiated service with a token.""" - self._instances[token] = value - # Also map token to its type if possible - if not isinstance(token, str): - self._registry[token] = type(value) + with self._lock: + self._instances[token] = value + # Also map token to its type if possible + if not isinstance(token, str): + self._registry[token] = type(value) - def resolve(self, token: Any) -> Any: + def resolve(self, token: Any, resolving: Optional[List[Any]] = None) -> Any: """ Resolve a dependency by token (class type or string key). Instantiates classes if not already instantiated. """ - # 1. Check if we already have a cached instance - if token in self._instances: - return self._instances[token] - - # 2. Check if the token is registered as a class - cls = self._registry.get(token) - - # 3. If not registered, but it's a class type, check if it's decorated with @injectable - if cls is None and isinstance(token, type): - cls = token - # We auto-register it to make usage easier - self.register(cls) - - from nitrostack.core.errors import DependencyResolutionError - - if cls is None: - raise DependencyResolutionError(f"Dependency '{token}' is not registered in the DIContainer.") - - # 4. Resolve dependencies of the class - deps = getattr(cls, "_mcp_deps", []) - resolved_args = [] - for dep in deps: - resolved_args.append(self.resolve(dep)) - - # 5. Instantiate the class - try: - instance = cls(*resolved_args) - except Exception as e: - raise DependencyResolutionError(f"Failed to instantiate class '{cls.__name__}' due to: {e}") from e - - # 6. Cache and return the singleton instance - self._instances[token] = instance - return instance - -def injectable(deps: List[Any] = None): + if resolving is None: + resolving = [] + + with self._lock: + # 1. Check if we already have a cached instance + if token in self._instances: + return self._instances[token] + + # 2. Circular dependency detection + if token in resolving: + from nitrostack.core.errors import CircularDependencyError + path = " -> ".join(str(t) for t in resolving + [token]) + raise CircularDependencyError(f"Circular dependency detected: {path}") + + # 3. Check if the token is registered as a class + cls = self._registry.get(token) + + # 4. If not registered, but it's a class type, check if it's decorated with @injectable + if cls is None and isinstance(token, type): + cls = token + # We auto-register it to make usage easier + self.register(cls) + + from nitrostack.core.errors import DependencyResolutionError + + if cls is None: + raise DependencyResolutionError(f"Dependency '{token}' is not registered in the DIContainer.") + + # 5. Resolve dependencies of the class + deps = getattr(cls, "_mcp_deps", None) + if deps is None: + deps = _get_class_dependencies(cls) + + resolved_args = [] + next_resolving = resolving + [token] + for dep in deps: + resolved_args.append(self.resolve(dep, next_resolving)) + + # 6. Instantiate the class + try: + instance = cls(*resolved_args) + except Exception as e: + from nitrostack.core.errors import DIError + if isinstance(e, DIError): + raise + raise DependencyResolutionError(f"Failed to instantiate class '{cls.__name__}' due to: {e}") from e + + # 7. Cache and return the singleton instance + self._instances[token] = instance + self._instances[cls] = instance + + provide = getattr(cls, "_mcp_provide", None) + if provide is not None: + self._instances[provide] = instance + + return instance + +def injectable(deps: Optional[List[Any]] = None, provide: Optional[Any] = None): """ Decorator to mark a class as Injectable. - Requires explicit list of dependencies. + Allows custom token registration and automatic/explicit dependency resolution. """ - if deps is None: - deps = [] def decorator(cls: Type): cls._mcp_deps = deps + cls._mcp_provide = provide # Automatically register with the DIContainer - DIContainer.get_instance().register(cls) + DIContainer.get_instance().register(cls, token=provide) return cls return decorator diff --git a/nitrostack/core/errors.py b/nitrostack/core/errors.py index a95f4aa..dbbf0e6 100644 --- a/nitrostack/core/errors.py +++ b/nitrostack/core/errors.py @@ -22,6 +22,10 @@ class DependencyResolutionError(DIError): """Raised when a dependency cannot be resolved by the DI container.""" pass +class CircularDependencyError(DIError): + """Raised when a circular dependency is detected.""" + pass + class ConfigurationError(Exception): """Raised when configuration validation fails.""" pass diff --git a/tests/__pycache__/test_basic.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_basic.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index 51d1b21acd612f09e8a23d9f8ce1d0451bc20548..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11792 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 R{+Qil+MpsaUVa&t|4-K6duspy diff --git a/tests/__pycache__/test_initial_tool.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_initial_tool.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index cd8dfb81c0872cea8cc26c7763627e52950fe6db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4757 zcmeGfOKcm*b@s#MpVWu4E!mFf_$N9Q?K*a31g<}^tweAtq>XzhV_>o7uB4?&E<3Zb zO-2Iz5$YB&P~}qf(1UXdQn^5LD01zg0aElLSu&_h60|^per~iSz)jFY-<#!%G$SLo zo;sr5%zN|Z&HK&F+ds$S5d!T$$=3>h3=;A;MEoXZ09kKHge(w~m{O7CB+<(yB`-^) zq~-#WH>2e=F%!%MMIXwApqGo`QY06Vh!rYq^98s1LVs)Qk4$0(x8&XFbF}*BaPDq# z_@n1=4*K)0-s)Ned$F0yqU?=)ijNDM)na$4C)ZQz&GnX2xm2ky*H_w>+g941+uroa zj#7WFUm`MjotS|+Vru^S+<>43fflkNi;A~PZl_GF-bwNx?C&SIBES{(S1x+2B)1EA zd(4=%Yf_pd`1c^b81N>=ssyY`eq+^cvulpzUNE}>4q9om=jsq4Gz78+eaRB3N>wS- z`!5tWo{@O?HG^5-cTB5Tle5{3#3Fi<1?U2?M7rfw7gL)TIPpj9k==V^to3r|A<+X zU7j?iUE?w+O&#VM`K!Z~sypSBhnacxvI}Z50rhsXvXv^0f)oK_Fkonm?OuOSBDik?r96Hm$gQJuWk}zNxZ`fE3c^o(i z@ls8z!k*ba1QXcSM*+09w-kuGYD%VT1s*7V7MiLRFawrmYF2PUGlO%gpBa9i3spmz zFz*}lf_(`hj{&9*_?YsTQx^UbIZ*2wwW-C@PC1=-%4NtFh@y&7;Zf6Kd1_ZMKQM8? zex2o~tdhY40^>ngBGN{qmOD+$`Q|whw&2OUgEqYg;3ARmhLT({g+TXHiB#p^D%Yiq zGJdI+YHFOu^h|p>u_q&Y;Y5$3a?C1DP`p9t3ji4InNFuEx}OJ|w$*lTX?wOgje%gj z0RBn-eUmg2WY7M4iPg~VwdDS_8d< z)Qp`0Fh~9x=>9AaUW@NsOC4D28{7!0n%0QOfdltnZ4l^|PP`%dPv3rm=z0|9*A=4m zJdOeCWf6+xF-(3x?K1&7fW#=q!;3QG;R#@ReT&@h z#;c&|O7A)tr+u(hr>%#OhCqbGJ8z9H5(s-LB(__bEN)I9IdT>?pkJvduD5ExUiHQazesk$0Pe z^!L&@I+9^o??sEurfnu@$1_a4ZSbrqz=7Z>g5$M>!Jz(e1=mQ1A#tSEUv^x3!p1qGRVI`qG8FkwRpM4?jE=3|?_K4q2D z%?lp|kGJiZHYl~PS!Onq;(-da;jIZnD7luW78HBp{%<|iJaPtVHGHKE%DZ*kMW_K>sO}jB9xkpYefe*7~i3@DQCK93RZox=v;wfRg@!Pof#7rO5lmJ!a3lC z3zRyP_v#Z41ygi*-ik(9xQ8o7rNSend_@OGUW2+Ps<$D1?XJ|PjIs%(5#s^3TCo`4 z{u~cLQ5BprXxUo2{r48O#fP%+hC?>(~Idt@zn zXf4^h5l~Xv%{L#%NVI4EJ1ddFx;nU|4nAYb(EamE>JXZ8eknRsSMm3#N${{uxU3GL zDaf(X8m`1K<|kwH~PxjbV~JD}B}2M*``u*@+Zl(ej&>hWfen1l9R}}5HR|ZN} zg$E5*E$3~AD|EUnGNYqdpp+@gsymfK4>)=Wk=mRi@T?OHs%N#emKjUI@la9piT4%o?1vr?o%B9sPf-I{uy$pC0nkt+ zNrJ%~xs&*ugg+;dFUbp#;*xqdbSrc>c`G?D%^zJDd2eLl^n0i8AN^qD{gDq&zJKy} z^2foCf*&V8O8z0Va%Ak&V=H^#TIv14D*53*NWM<;Uy}Z(swDM3CIB`xNea*RZ@@VG zSR-<5zGqcRJ(a`Kfd&BpUkq|z1JTFZh#XoAZ(r*^u+(*MEzz|WOTcd$-x1T1f$ qjP)d-4Md-41Jb1Q1WK#1pVb~J{hz4=kJMcag{T7}2mTARiw&m$ diff --git a/tests/__pycache__/test_oauth.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_oauth.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index a575ff8b4c8ada9347e1ba07142fea77acebeca3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9410 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 diff --git a/tests/__pycache__/test_tasks.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_tasks.cpython-312-pytest-9.0.2.pyc deleted file mode 100644 index 505d9429ea375559d2e9d2a2f5a10e46c7c0c54a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16390 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 ServiceB -> ServiceA + @injectable() + class ServiceB: + pass + + @injectable() + class ServiceA: + def __init__(self, b: ServiceB): + self.b = b + + # Manually configure ServiceB to depend on ServiceA to create the cycle + ServiceB._mcp_deps = [ServiceA] + + container = DIContainer.get_instance() + + with pytest.raises(CircularDependencyError) as exc_info: + container.resolve(ServiceA) + + assert "Circular dependency detected" in str(exc_info.value) + # The path should display the circular loop + assert "ServiceA" in str(exc_info.value) + assert "ServiceB" in str(exc_info.value) + +def test_thread_safety(): + @injectable() + class SlowSingleton: + def __init__(self): + # Introduce a slight delay to trigger concurrency issues if not locked + time.sleep(0.05) + self.id = threading.get_ident() + + container = DIContainer.get_instance() + instances = [] + errors = [] + + def worker(): + try: + instance = container.resolve(SlowSingleton) + instances.append(instance) + except Exception as e: + errors.append(e) + + threads = [threading.Thread(target=worker) for _ in range(10)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert not errors, f"Errors occurred during concurrent resolution: {errors}" + assert len(instances) == 10 + # All threads must have resolved the exact same instance reference + first_instance = instances[0] + for inst in instances[1:]: + assert inst is first_instance From 90b26afbeab2d0e6112d2eb44e9e18d8021cf6d2 Mon Sep 17 00:00:00 2001 From: Gautam-7-7 <140533905+Gautam-7-7@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:51:44 +0530 Subject: [PATCH 2/4] build: untrack remaining pycache and compiled pyc files --- .../auth/__pycache__/api_key.cpython-312.pyc | Bin 4151 -> 0 bytes .../auth/__pycache__/config.cpython-312.pyc | Bin 4059 -> 0 bytes nitrostack/auth/__pycache__/jwt.cpython-312.pyc | Bin 6834 -> 0 bytes .../auth/__pycache__/oauth.cpython-312.pyc | Bin 11222 -> 0 bytes .../additional_decorators.cpython-312.pyc | Bin 6896 -> 0 bytes nitrostack/core/__pycache__/app.cpython-312.pyc | Bin 33340 -> 0 bytes .../core/__pycache__/context.cpython-312.pyc | Bin 8564 -> 0 bytes .../core/__pycache__/decorators.cpython-312.pyc | Bin 8239 -> 0 bytes nitrostack/core/__pycache__/di.cpython-312.pyc | Bin 3734 -> 0 bytes .../core/__pycache__/errors.cpython-312.pyc | Bin 1666 -> 0 bytes .../core/__pycache__/module.cpython-312.pyc | Bin 1734 -> 0 bytes .../core/__pycache__/pipeline.cpython-312.pyc | Bin 14141 -> 0 bytes .../core/__pycache__/task.cpython-312.pyc | Bin 5304 -> 0 bytes .../__pycache__/event_emitter.cpython-312.pyc | Bin 3572 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 4812 -> 0 bytes 15 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 nitrostack/auth/__pycache__/api_key.cpython-312.pyc delete mode 100644 nitrostack/auth/__pycache__/config.cpython-312.pyc delete mode 100644 nitrostack/auth/__pycache__/jwt.cpython-312.pyc delete mode 100644 nitrostack/auth/__pycache__/oauth.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/additional_decorators.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/app.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/context.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/decorators.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/di.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/errors.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/module.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/pipeline.cpython-312.pyc delete mode 100644 nitrostack/core/__pycache__/task.cpython-312.pyc delete mode 100644 nitrostack/events/__pycache__/event_emitter.cpython-312.pyc delete mode 100644 nitrostack/testing/__pycache__/__init__.cpython-312.pyc diff --git a/nitrostack/auth/__pycache__/api_key.cpython-312.pyc b/nitrostack/auth/__pycache__/api_key.cpython-312.pyc deleted file mode 100644 index f3ae70a906ca138ce22c1d4e619f2a5c658f74ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4151 zcmcgvO>7&-6`t82a`|IXG9~KAipAPi?2St~mK!^^Wx28y2euNWj?+U4r7Xi;Nu>Cf z*_CYybSj_%LIn;~15G8v2<)Q`r+^Pdkz;ZUdXXTfVx|ddpgA-*x-^iur@pt#r9?tW z&`U?+o0&IncILhJy_q+E-Lb<WYVue`3cea;;HafW|V>h(^;SYS%PMt-cTy zpcc|%Mv6)qIVw{e7t^kcJL^F@-orn1){m`3-*_zUV-Rt&Ha&bpV}Nt z#se~LN9KNQ6H#A*s@`v~H?9g?dL?O?TzM^TCUe<%TBY2T$!W7`9a_H8^JjBeGoH-q zEK!?oRqk4+YC>a~=meFB3Lb)gT8le8f3lNLZ7Eugsv2tIv1~k}bJuj-nAWv~RNFXKXgd*p)d?WWq)3Y-1}(c31?bgm zNk~zIUd`5oOcCheOsf=03jEz%9BWfjAou9FDl-&fh!N%k!sQrln0sUCTwJr-s>CHj zPfs>U8o1{$l>eMQdt&03hR%$MsralJpU5UnmNU$FVkVL=nA5pz#L#B8a3?@&dLo`r z;^znQ1+K(mNthIi&4n6_H{kGbkH$}d%#(_rgb$UZhe~gG*Rxo6l|#KH>AJ5R-UF4l ztoWA#i-FIT?kdO=?qa$*%d+E%y3v4(z_(0%0=UayxN8&x752ZLVk|DJtHoBOd_XAeeJdyjq| zI96_LU-sSbt&DuoQjtl={)$3EJtgUVFAKuz4LGZZ@(CD$P$sFZCU%+9JMTG;MZ*9a0RSxg==#RIsCmKYlMKzlSg)pJXE^Nf#A~I;@qmzZEc>5 zVEn|^J=7y*5@s$Fv%dfBb_yukCQdZ!pvb9Hbqv8rYsIm`B}CPnJZnxyUWAj!jR#~p z`4)E>)A7Lh*=8a-fMW_3eH8FG@HcX9{^zkr_GZu z1EHmN7T;M(+)mw0-R)f8eRw@ExDgmy3kIFt(;tm ztOgFODhF(EZ-s^gvn{><0*!a+yW)=l9FIhU7T`YH4m_+M_}jo|u@0a2M6(W@k~m2< zVL>ShqxArC;B|>?k)t3dexIBwjh|}>SX}=I{Hhm#d_cztwZitz9yny_4|{eI{ORO0 zf27NllIy(^9Fl($uTU$1?+GjdU*#={Tn3z`$7=8uxiIiN7ZNkvZ)XE=8RlmlpUzun zANLHQWfKnuH#(1g+V_+l2SHV0d!VvX9;QNzi}7p$xb>5S2NJpLWO8bj>2?~# zj^P4msqk;YKexT2IL2>2VI2M;obxJ>r*JFv?7khj8M$?+6!;>ryW%44-H&{MrIYWS zEbr>Oed^|^yTSEckZH8MaOAnU=4tNx2tEZ!5jd*m1AV`DG< zYR+>t63-_iaMYcH)36=Z8;3MmFlJOgCRdnh#$u4u#q1Os+WuJV?b&#`HX<4(jVy$oA$qI6j;LrFikOcy&g;mJT%ALKBJzlxzQFm`e zh8p;x?ur{*3Tf-8;#jrI58Yl-?pfKlV%$`#1ez5ul~1pntq`d0oULN@*d@s7og)-gf1dP-Rieae^0@-{o=wj`~;DQ%} z1TO{&UJSDRVsP%MsH-5WZuT-vY)Xt9lKJ}m$*dJNj}S=fg`)Toy^m&wa!oR8Eyy+z zWlhCdce}fDGlwin&Jl#!gmsrflr^UKfO6EEVe*Q1g$d^kWM)}-th6wF3Z7l?IspLB z>Z;7eMA~SYk2jO`wrR@GxH6ezF_z1ja~)ePKj5&3VL=0EQe#S{43*?d9+5lU>9JF8ybq%)ILfHP> z1Ffn^Ty2j9Xj$J?{npXW`f-g;AlzjZ^2zKJL!UBqCl75tiv|)orVrS$$PgiXXY-(z zwESax%Gz+F5?mRw=MS?t-~*VJ8EA}(NGbh0X|eu2{~$m4il|?a=f0H&>Fe|{vFJ2B b)s@n(AJX1$g%%pD5FntC@XTZES_=Ia6x?iA diff --git a/nitrostack/auth/__pycache__/config.cpython-312.pyc b/nitrostack/auth/__pycache__/config.cpython-312.pyc deleted file mode 100644 index f7f1b56225c5d3472aabd676f63a5ae050feb956..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4059 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 diff --git a/nitrostack/auth/__pycache__/oauth.cpython-312.pyc b/nitrostack/auth/__pycache__/oauth.cpython-312.pyc deleted file mode 100644 index f451c9a499d8cf951f752151534af65f0d1e4abc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11222 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 diff --git a/nitrostack/core/__pycache__/additional_decorators.cpython-312.pyc b/nitrostack/core/__pycache__/additional_decorators.cpython-312.pyc deleted file mode 100644 index 7c978a446a67d45060fff53bba5f0c1d1e05958f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6896 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 ofueMtX(UhOe~f}ZmK0d{a%zyJUM diff --git a/nitrostack/core/__pycache__/app.cpython-312.pyc b/nitrostack/core/__pycache__/app.cpython-312.pyc deleted file mode 100644 index 2f7c654650a0ecd7c508914ef42d78c63b7cfaee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33340 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 diff --git a/nitrostack/core/__pycache__/decorators.cpython-312.pyc b/nitrostack/core/__pycache__/decorators.cpython-312.pyc deleted file mode 100644 index a92614eb3753c223d1541c9a23ea4be69efc2053..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8239 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 diff --git a/nitrostack/core/__pycache__/di.cpython-312.pyc b/nitrostack/core/__pycache__/di.cpython-312.pyc deleted file mode 100644 index 0d33e9b01369bc870f273accf143527b76556411..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3734 zcmaJ@U2GKB6~6PgJG&n5`Ul50G2|KpX8CbaA}W;-P-4^I0*-{q11q=N$;`DGvpehD z83Vi8t*ppZ*A=NjYGq3_lB-s&5q;<*&&f-wK6H(VFw;s!YWmQ(Ev9OezVw`#o%Ldw zUTMy~bMD`{=X~eqPpz$S0`0e>$EVj}^mqITn_M4ko`b;>F^MT;Nlr)$IWaBfq_mWi z({fHpD*_S8d&Csyi7DL|gI(!}88sCqF`*Y2lcE`!SJKgfoKpXZ3sQBO(88Y+0;yFUcO&nl2(AzYRXNV zh^fqzbkvM6)r38hvKgJ1(=k(JaWe)V@*-v&cr89pWN}#20{>b;NF}_Mkr%$?=7A=EF#4xcQ+xA{FFOQb}mq2J!-G!bS(IZGtdSg`Z@adJ->yC-7)hBGr<#||WTst;XaIfa=V~%;Pi92EBIh!!8 zp_ziGO)e=2G`6qmE@yWtp3|sS)a#9-4L7v&RI$DJ89qzHg)KnlNG(Eo_f(`$T6rh@?<^~YnI&R@m9}i- z|21q~ARNz3iC`7ZrU10O;Kdu7bW(yRJ1jfdES;KxdCSdYio2Qx47vCMrwzy)**JW( zBHv2*-6u(3P>wT3-KAg{;LOL`KKKEwfo69?TS+(t^S-8cU-Bhooa%Ij=dW8P42`Vr zI74G8fp^2Yl*rLb{o5S8jnY81YVboT7OLTXzS$|x(=@P zGP|HS;~43h&jlW~X(i~JX-NnzQx;3&+(47f1h-+S(5AezB)~c{jn2ERZP{3Wlw(TS zNG@8I1i+9VVI@;833rL9d@6-Eek{1?WqvC`BAO!i>xe4LQ>3hvLrBGnKaK;&{03n*pS{iiwI-Wk zw#jUxKtC)57^a8(I6x7v2zmf71X_BH6;eZu6Gj8qKu>UjxBdTw!TT)RPZQ0)0r59y%w*t6 zPI1q7bt?zGr$8WI9ekJ`ScQybp-?%2pB`!gM*r zP)3Izz{x0V&|Prvh(3dw^rDbtP`6&}>`jBs;F~;%_O_m5nM^9yDu>tl&q0RqSk5Lj4d56Asj15E*J3(b{j1EWLl{{k4{X zjn3|+Gk4CcoLK8jRkakPz+~s5ynjzIFSy!E8x$ZGb%i3fw#cdk~CTI(ItRc-otgtT?vc9zet zcC05Mdu{aWuC)H86OP!>dLL_aO`|Kje%JS_zExwr|M-{Mn-Gz9_0&`;HX}Ug-o1RW z+I_fkt`;Zz4mX9gjbwKv`StS_($@Qo2)MN4NOkD^7sG$P@W%^ZoUUFmswS%%lhvbB z>m66CcD}0R|67xA`qR$GE}=kf6El zksyg<^zrZX39fNCka@Dvwrg(msnYgLl$1T25{b0ea0L2@v_BKY1`kHhR8cur1F~;< zq(-1yxx9&8O;waPR)%W?x<5#M_dwpnzgpse(z85XBhamkKgTW*2VXzNhk(9N2ccZ} zZ5{&h6UYk-WKNtSKZWWIvvklV#R{1epw!pOLP;1WDXI7aKT?b^a2VaiaxG7DI=@Es zx(n(K9kr*K;p!h|Sr8m9v5#ggfG=e?XR?-Ixiky46wOa=1u2U;!P=k%YLs9g1`3H` zdUFEEF9Z*bSQ0&S4Aj}2!^9_%o0?^|- zHEJ=anC60tDQ1R>DDJVgo_#8tH}tG?dZ;lyhCA`(uc7km>8nV$TE9{|AFFgtrR(ax z$Lf(a^~mb*BlQg^v@3DnbWpA#*5GoymwyM3z}Nn;UarNrZsaF{{~j(F00MUas8nsP zw2^GD$Td+`I)Co|n?MJAt*a{U3b5gqx2hL`i~yM+z%!Oy9CLaAbW~iR4ZMPX8wr}D zUNN2uyy!GP1w2%zgLne(6okK#*S{pM|3`jT5PJSae0omQgs$6{mj~`#c}`&HpY%Wc Cy-sZa diff --git a/nitrostack/core/__pycache__/errors.cpython-312.pyc b/nitrostack/core/__pycache__/errors.cpython-312.pyc deleted file mode 100644 index 734f4787b14c907b4aec22a4fbb7fed4425606e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1666 zcmb7^L2DC16vt;bNwaC2R;xvVm+=@%a}Ww5QpL7PJOn8!7y`?5ciL>bnc2?F*2Z%W z9=)|E5j+(?ieEr;@!%H_5-*;dH%T@VXu&M(+c*C=yZd|d-psdhc^>KOOa1=gPZyzI zFc@Ejk<q>5K;b8`?QX6g8a?HQmr@=DKb**IRkTdAxH< zgH%PF?Fhle`+K`2k~GBc+LU3X1lI@e&pA_=RE<%GohK%Uw%X4C?u2IH|*RamZVBzE-+7(aUp#ygEs9D ze3HFdYluAjz68dAEPz)Y8)%>YnD%i-3-Os~S9hu8sR(F;t7klA;jmsco0(uik5ej@ zE}$4q$C1R0E4&ZyTmuXJA;3Tq+0s79U?DUU?5&rAcM>&?I<>quYGNj1MLS7M6#XCK z6(9y<)_Dm=X6-?miL|&~A1>tDQz9u2Vj`tpD}h6rP!>`abaBKEX#j14N8NaJ$!`M% zn49{}O1_b;B7|mQt88ZsQ2f~1rcb8JZovkGLAm#+>45k|x58c*t2V{kbsTV}NW`eP zrV;Ky3Sd_Coi)OXECmS7l)}muXZz7XDy(g`X0vOeN!;327!oBpS6Pl|n8Sl4I@6(oMYf);3ia z5p|_d;mwIitsqhly;kJF|L6q-2V+GFQct}(+DNIY9y(+1(hVYS?QhIr9 zd|m-m?ha}XpCADKWJX8wZKd@qRdxUd7+RnTby($e4l%5#?B=NI z3JgRBj6iM=gI)^1gBC^b&;0GrSMGZ>k(E_*%{N!<3Qf|ZZF@9btNY(N_Oyo|b$M4x zj!Rb3lxAvmS7sYBU?2AGQTPd*D)}dOPDaP~7mi17HNM;~Y~yDmN9fkcl?+egPEvyH`Id3&O%AEDXvu46Roe$e^KlFM#LdbD>BRAMLX2s<$2 zQR^EDTVM-raa;Tr+5t`2;8wv?*yNItYx2p+H&HTDe)*j}K$#0*fC4@M0aq3|7ReeXL{jS(VHPw_3GfBjv>#Ws0wX5}0>RIcj)OPEqRMW<4 zyZ!0Hh%Z@QsZH-BBZ=YweOosu{37fC|7^A?G`XhOL{0t$+-sNNG2DO~z&MN905oxK zNfX{%K#fcK81);~QmsQenmJ{)Cb=W@tC5(RY55t8Qa&XuVcqaplB(KZ;tsJDGVau9 zeYj1UIm;=TmN!3hHl?zqpHld{0bcff6!r~_3orE~8|6 zLq)${tJteu*OQely-lpkNosD9X$kj}VrJ&jyyxAkbb__kdrBeZA%w5MT)_I(@+)Der| p($Fs2U4E_{_KwFAb7eqJaU1V`_Uz$d{z@z}m(uNe!*)z^{SS=EoOS>J diff --git a/nitrostack/core/__pycache__/pipeline.cpython-312.pyc b/nitrostack/core/__pycache__/pipeline.cpython-312.pyc deleted file mode 100644 index 879e925068bd19e03091931739a9d2fae1ea1eeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14141 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℞Owk!%)Y3X3-r(p79GEf;YkD7LK4hvlX3 zV43_ZY9R7e9K>HBK(3dNhSFsOXA$5oR$m%rq--zZP}EA~u}fM7NqGM+y8z(1EFBIV zIs_X&9aO38*i7Wk?D*{$25-~kJ#ei0gh=9FmQ{z3CLEP<$F`_r+mbitxIcV2;i`$d zTB5F&<*AsfHD1yh?oo=$@04tdG_=J^+9URM5Vhs}T{~6kTI~MnOxXIC#r|gli>_F4 z!*ao!#XAy(Rg2GFx-Z=Ib63rRG2!In&c>*-ak&&swgvW%vu=52Wpvf~M+=iAfr$%G#uns;&(`x>zvZ|AF3MYD<<-rFCKJo$Bq&)}K}Hg*&>m z|Fl0=ejwcYD;=HJMZa5d|20>vqHX?B7$N`AT`N_+eQD-rRlCD|iGuQYL2b04cBw5^ zushs|752Paao;s-tl}UkU;W^F`BX{mSD%U7>LSLvpzy! zod0rjcLV)5bT_a6z8+#f?5GELvy$$v)!(e@I0%Vb6?At!d&|XPyp~2-kFamXpdN|& znX;6-9bAp6<2O$pID-4#m~la-?W`ygI z!=%R%WVr5cF^OIU#jL)sQ1=GL(-;^N;b)=-PHTZbeVTCTA(bbs+1w}n2wMCm{N#TG z@SiG|-L0hxTycA2)ZQ4g?|9+p2kb7c{sRg?@-%Jb9B&mj#fqEbw&sYjIbv)6=#Itl zK4k{HrSRuf+ZN0j4l0j1_bjkU9a9jX;lb`-m4CB*xdYg=rZryC8mVZFJ6b=yjopnZ5)(*-N8Uf_%{US^-oL&`b8l2PW3tl|PN%&qu zegAC47z$g(gwH>N@V^9EGGjXXX#oCFq9t`HtKK)Gp|2EX`M^jK!AsavDap+s>?=!X zzN&w5;4#*KfXM6nG4>Dwu$d_Gl8?M0BafTNV;gdZtzHhyLlLe_{s zkhgL!6BOeC^=}w$(k;kEdr|%&Kv+(iew(WK1$8J+9eSIp_yyJRHnrnzYWH0|O&_Jh zkKCp3i>W8+g;GpCNm3qqp%GIa8s2*9EFb7}bVHH?u*N_V-h$E%ca1dNM~C-eu|861 zfL=I@sR24^X6fQZcJc8mmNg2ZNh^RQc4;um-=Br(UapAN@68|3bA_4<4$f%if~^e9%ePViZ8~3<#V!Z diff --git a/nitrostack/core/__pycache__/task.cpython-312.pyc b/nitrostack/core/__pycache__/task.cpython-312.pyc deleted file mode 100644 index d3cec68211ee827e3bbfa64f0d842148dc4e407e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5304 zcmdrQOKcn0@o_&SMXpT!{G()vGD*u+B-^cPr&b)dh9%c<61lAt6eR>~8SY9Yy(HDQ z%Oqsk#06Y{22xlC3Rp%^L`NCE2t^M$^w4`RX61nF#zh0Pmz+vV1p@TcnYSM<720vp zTOYugd2i-@=FR8L@=yK!A%g8xD*sQQ{?0l!5Ny2r6Cmypg(yCaq%G`A`=HNhT!ByX z1tBdI#I#uOr~N*QA1DOVK_B7B_lUyZB8u>kvsO#@u`v^}Ye!H`q0OB4|2_7lUDWbxuyuz!3Djo*TvY-gJ z#Izr_@SA;aX7n2iMT1rr&42;4oKsBRFtjXhuZ9R|{Ow-x;mS2%jmS98nu0wTw|gA) zY}E)coHLQUOF5E+pZmghJ6DLw?^{&9J+%+BIxvL+%LHr zSM(`7jJoGg1n9Z@Dg=6AX9nxJ`zs>w`W>Ee#G&%z4%NXAZ4zb-IHMJk^23$CTcS5| z#g(drc8KXlh8n7pn*o)+5JB{EYLMdLOh2%UvTp9Puv}1eJ+q>kK_z3TMy{Zme6jSd z8AP&LDnhqxWX({Ps=!y|jA2H!jBd!~Rh)nW6xB5QS4)~E=Zc0(-^pmEpQ?IUGfYvX zv_wrQr_0$)F{^5ts+fU{UMXgCB~x5@M=cs=NP&%H73maj3c9K-v%|8Lb@Z25EcWm7 zm*$q<(p9Q2tz^nZW~rDnXbH5)-k4pj7(Xo)XLaRPok3_&)%Q?=ww~^bz~RLKpyxOQvp11F;h@w*$l~Yp`?^Gq$OGYSvjNG99}ml z#dt7zxOqB(lL?IhqUb#oos zZsvom$Tmd3tfRumxX#-B#()9Pjg&) zuR4(CH1rmxlpiu?S=CcMrk9RKuuT*~ABp>p0Y|^=x(cjdleoYF0Pv@Q;d|0uY5hmr1IfDhm+(LYs_8SPJF+<@ToM}7Kt?YobchmCNE$c zs6(g*RTS5;jRIqxDhg^8PIkl1aPL0vQlO08@?nSSV9yM#Fg9 z_xK2N548q$>*VPO{foH%A z(y8*Q<6WWi75-UVBi`$plGF8aGcwtTOg;*p*%AS;(4hDd_0C<<7mE+fUGLTSUW)4b zA#Qk2q+o_~BZcKVH@6vXe(IH}@@_2;$F-YhMvt$Fyj#mV@`Si?!*{+Z;N&A635Uw# zwd`qo;)Y*@uqhQ*H5DrM>L}VJ=JsQHIh%!4w_Mh=N=ourxf40K#WNEt3m=L_Ojn9! zb65=F#X$_&Pg=jTvlT|sE-&fgy#n*}B7k0jG<>Kzoc!}}vNbWiCjLfhP0g=~j|UR1 zi6nNvKI`@lsD3A)=pop*x3k4w+}VEq3}-v^6=&ll%@&zfSMl{&js-dcGyb-{A5@i>fz|91zQ zgzZxY=uuQ0(;!VCz{8gjFM_@P$pWu2yg^yl^eg4s7 zD0~tDOVSu|6ca2xhu{JLQ+T^n(&%@wk98`&ga9vsP9caPU>}Vsq!3`T(XWEH|BAHY z$(xt!^J~LvxwUb4e71$;=i-$8cDWv6VmApXF&k!=&iTbC^S$@~uM z+rr3dOKh=#7{>zw+Dz;qvPFPu4>1W&JiE!a37{=u2azoSR6EEdg0Kc`1ZZoRp-)D3 zuxASb)gEyLVJR5cwB_Mkj^I-cDC?T>a-l|Hl`0nQrr)Tn!k;&m;hFK6iTudxD`Zzs z0AJNjks<3lk6EePa%S+n^*_M{`UcDd!x8;BfHv>*`933&&&a;Nk(q5W^H*~COF{BY SKP3RZ9DBtV-ys0l;r;{Wxdxd4 diff --git a/nitrostack/events/__pycache__/event_emitter.cpython-312.pyc b/nitrostack/events/__pycache__/event_emitter.cpython-312.pyc deleted file mode 100644 index 7d8820e7dfef704559e36b63e2b48bff68200983..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3572 zcmbtWU2Gf25#Ht9k;fm2lu5a=BE=^usdJJgyGqj-w&Yfp1h+{ex3K$TM)u{*JIbW_ zqwbw7i=_gg5mE|JVWemy1!+(TGM1aEkuR1JL*n*qMo!E6)AC%vqa;TiN@b?IXgwYnxOmQF3r8nNBs*__WT(q zrewFpjb#=raa>KM)GI07;wKU@)8fCLFicChFgFV#@!YJL$Y#`3%-PJL-s)iIU0~LT zPEfC?OQTUv=e1TipTARG5*0ua)c6L8TN9Q^)T4zow=QZP-K&YZPxl{!FErRAE^6Lo zAsVR9!dzAEha0rm@<3JQ)>=T;3b$&r0+ZMEj5(Q3n5NFAEH~55IhKjhswUW>NjUiL zyar^A;Nv6Nxp(bC_~j-Q{7{??nLE2Blf^hwv@4<`%bt8Z)v58PZ_SuBcOJ9x4E?Q;NT5PXfd zUm&an#H>4t9R4m2rc%W%1YhyiUnx(eKnT37@|`=2Yp%o<5FookQk zUaV>%yDXoQ0;K90u&fnQu4Lyj+O9;V7H{aOc%zP@0@h2Iijr$@QnMz;ix_P{K-1UX zDDbPn$EQXM{6n$Z*4Xmdj!`md8e%0rmx)=vSV}bv#ax)xW3|AA?+NlY&fW$Fa7On> z&UMOyGyegFc{&2$?*Cn$Vz4Cfggq_q%5!noovO7csauP6z)H)l&d%x?P3BoA3|SYF zZY0kGF;Hs>n&Z(nrq7^#S>33)=bOy-Z}WJBaRSH^+4i@uU-*Ucfl~4x-R?hHn7Dav z<=pyfrNEJGF>o`u5?uF{#J;lFUlRK_CN}$v1EUYc(~gx02|va&(#cA%b|<>e|3bX_T_G$=WBcHI)P{CHp`)YH`VUz2|7dScIWck1iADDLKb;1+4WEo(XglOTAZ~( z0_XGfFn9+N+4n)e_qjFhT|oeSGGg-zN>*RyAy{4|tD!z(Vgj|>3xrKA3Z`AHQIHHc z@51`i-rHO7iOP5tyYLAxd``I6y6qASDb#H&XXJ5MKVu+ z!_8BfpUU;xFp#P>L(&<`vXjzPHKU~fXz?tQj^untq_~<$>Do!jqCVN{fRY19S{}d? zWHQqVCyZE@W#`O9Mo0AF8^H2T-iYb;bHL(^1;cV1rUoZi!aPfux+TO@bH-KK-GJnm z;3}4PH%p5()}p$BF0=t%MKq6E=h~WpjH1uOI^&l>{zN_l2uC`~5xEqR%aOrSWN;(- zQSXDuXu zFMjsKPY(4~+@$^J-=4Wir28px!Iz=%n(sYdIV6=rQn7FHZu0JQ@tbcJr!N)Xyj;Be zR93@*4%a<_jvaL0bGINL=0JS!AVt2z zd*+yMuV;)u(=XhAQGkj2{Q`*YAHzkmi}_(6hM=qRQ+DB%gBDaUtUi!Ce2S9JYI7;% z1B&v!IW^_XFodJ!w+mBRhv$XH!noZ*B+ua-XPAs(u4jmIhJnj^kQ_nsB_Own9o%*u zu=TJBW+3AyK$gjND6;gm$3p0-ixawcIO1-rAXo7L_tZt}GT@%NdExYv04IETW4uD3 z+q_|SAAh&_?Tf#?Wb?&%atEg>K94ZE!Bq%!ni{n>#419PkN!x6W4xbek{R z-A7-1h9gH|w3%JinqC)pL;UYZKz<0xNTVijPK&z=Bu+Kga$pgH_B@ff+&6rP%LyGq zKs6y|%&e4F*)>U(;1zd05z{3Q#95YL*A*mTj$H$vg1NxA?m0e?-R6_Hk zTXrr3`6*o^EAT|veQ281w>xBGeqvWY{uDOXe5=5sVU?1P<;|{W5_@-NDs>e zeoO57RE(6xo|4$}0Bij#CGnNb`x0$VZ$l1MCjZm6+|%#4F_8}w*UhRKJa z`J5QAadQfXcnTXqf{#?k0^*1hYNy#a$SC{`%xo2oQu>Ja9+B4HlhHC6{VP8}`<@UW Ql_(Wy`+E2pfzICPU(=NefB*mh diff --git a/nitrostack/testing/__pycache__/__init__.cpython-312.pyc b/nitrostack/testing/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index b3e6d2d16a29ff9328fa652f1927de2445da94f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4812 zcmdT|U2Idy6`uR+|2T;q5^w-RSinF`fMw}s>9)b4%kmQ{0;<+ZYdQDc1c&Qu@7!xh zQb*kaT6wjt8upOk_qSStiM3 z*(A$gomIIkpX9SbQpk!)G3!dY7{Zb_h|FCkGJlh`H6-0*LQMPzR${ElpB~FACfB2l z#dy<|mGY@{K5w?1kn%nGyqb~HMkc3yBP|&@I%fL&6?$HwiJUf+88#bgJ4v>Bn~Ign ziU!P8qL3sblO!uMNlxKqRuN=QCPTc;U*>OCx+X$kIlRo=VsBPjZgI{LTZhiWp$m@0G4cd!2+r6M=a3u$e2Z0Wm%ydI z3-kP#y~@2S2xNj|Ns$yeXSRNFfc(J!3+@6F6Z^(}Xw6RDa*r5e@>)8pfOm<`9_^MI z7jgEp;)i*kxX2h2V;}E$`T*k+*x69x2i2UER`u88c)Z}FeKF2-r&2g!DrI_86|(~G zPo>^2q*c4+=VS%9`8bj29ykN5(Fcap1tUG6p{MA0*q!+?I9sh#m(Q(|4&cV%pa)W^ z4D?N<;`uStRhgSMgGyY!jz+yq9{3w+D{yozkQh%;0ehMp zxPy)XjxI{uP`(bx_}b^1!2`|uNg#hG|9Xc!5{b8I%6seJ?6((p9$WMr|CD7sO=SXP zg(Kb*Od0FYt7XShxNAP#HPgBfes!tg)e`sTVDsd@_eV>?GDm{lkM%~-dGV_~4a{$u z9zTDZ1c0UOAgu9E^i}l16*RzAQe=uG1)N>RNQ~)=*`p9)bVVJqROmRYvOzpE+o0o- zXYA5x48aH;H~S7*4y`HiKXls$ZUB+#Nz>s%R?!Syss;qe(U6zmxAGfsF*|sQWCR5D zWn^{8RT_2QlK_z!$JsB!2EV9=5j^m^0tRB8P=vr@YXhsuP4EV8(keyHux(;2t`ij(2r`g@CMq*Dpf5#tF-XW-YQ!575R!5!DwGw!j`3QWPcjEz(aekWua} za)Gg+MHuf-q*YaqrlUqKr$z_Iq8QK7j21P{Dm56s3xF7b6j2%5X!-&|v8+I}XVPkB zT#=!NUQmtrp$ts5_%~8_>xTL0VnWi_RXEZ<+O;Sw3DCAYRVpT;mc{QUOrVD0F8wRBv zmi7@nrj>kF!ZC*_ePh3o9_iT-*4V9xABdI z<|A|dBg^5ot4FRJSqg8P4{uuv@0kzpnb|VySqL9mYB*Hl$|7ltl>GO7EtAsq{foX$ zA4)emes*@I>8I*)WW&_k*Ip?d{{$M<4|^AVv0Kv2u7&v5KH79Io>&e?r<<;vEFHcd zTr*ji8d?m#v>b{4r2AU;r28+8kuu9QZCQ?NM8Wjgg~;x?@NSr_F;w!FH<4h1`Rq|U zY1{OWFxai*gOgvC3f5-1jtmE#q zvdFJxkYC3EZ*m2isX@Ieu)GXz@xKBrPG~&^E9@}b*{fiMeF3agZ~iK*ur?rQ0A4SI z7Ou!oa9Z0c=z!#|3OxQI|4iuMiX8pUDp0Cs7nL=9vkGm3%ua}_vEvsr*MydmIJKBG;FHSeZ%oVh9R}exsT@}v4nOM-mFS-SK z(wBgk0Z6Fil(P=H4I8#2i6Pm6WG9mSNM1pLcx6Eh?L;mP#DS@ODA@%w@BDtNCiRIQv=?Eg(#B^pc*tHzl zz7*-2k91ve-*0N0T7N@aY}#!JJLe;vlkNw>*12HE3t(;^3BLB(qxkbt&fD9@-1alQ z0)IPjXbV*C>}~9Ik-IDp{9Tu?w}rdg%pxCVkZ<9De|F}yro?rnn#Uw6S<2BV!2ACcNU(Z>0w2$jb%p~ z^VCPLcUUn}_SpK+wW9VlSaM9L%OUIt{R%W$FP2pq!aJvaNlYg?D;4Zs~*i>d+ zLff=lCh(ctop^{}c!yOQ%FT?>I@PyA;8PaELTEZsCh(c*wLY``D_ASpl?mmkE4kPFtT@=@HiKmEn5&Vjfd8#^B$HVSAgnKr^IOU%7E9N0m6FxAx-{ z-=+2o)iQ;(uiCwu?t}It{&kvw&iHEA(RPqvG0exL{bMq)Kn6bLPccmBAwg=b=0A-Q BMb!WR From 8f8d645423a0695700c80b089a172341af12b815 Mon Sep 17 00:00:00 2001 From: Gautam-7-7 <140533905+Gautam-7-7@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:38:50 +0530 Subject: [PATCH 3/4] feat: resolve mcp primitives and execution pipeline gaps --- gap_analysis.md | 85 +++ nitrostack/cli/main.py | 651 ++++++++++++++++++++--- nitrostack/core/additional_decorators.py | 265 +++++++-- nitrostack/core/app.py | 137 ++++- nitrostack/core/decorators.py | 32 +- nitrostack/core/pipeline.py | 35 +- tests/test_decorators.py | 185 +++++++ tests/test_pipeline_gaps.py | 73 +++ 8 files changed, 1326 insertions(+), 137 deletions(-) create mode 100644 gap_analysis.md create mode 100644 tests/test_decorators.py create mode 100644 tests/test_pipeline_gaps.py diff --git a/gap_analysis.md b/gap_analysis.md new file mode 100644 index 0000000..7d13b41 --- /dev/null +++ b/gap_analysis.md @@ -0,0 +1,85 @@ +# Gap Analysis: NitroStack Python SDK vs. Requirements (Updated) + +This document provides a comprehensive evaluation of the `nitrostack-python-sdk-fork` repository against the design expectations and requirements specified in the `docs` folder. It outlines key implementation gaps, bugs, and missing features, showing which have been resolved. + +--- + +## 1. Modules & Dependency Injection (DI) +**Reference:** [01_introduction_and_di.md](file:///c:/Users/gauta/OneDrive/Desktop/nitrostack-codebases/docs/01_introduction_and_di.md) + +* **Auto-Resolving Constructor Arguments**: **[RESOLVED]** + * *Actual:* `di.py` uses `inspect.signature` and `typing.get_type_hints` to auto-resolve type-annotated constructor dependencies. +* **DI Thread-Safety**: **[RESOLVED]** + * *Actual:* `DIContainer` uses locking mechanisms (`threading.Lock` and `threading.RLock`) to ensure safe resolution in multi-threaded contexts. +* **Custom Token Mapping via `provide`**: **[RESOLVED]** + * *Actual:* Support for registering services under custom token names or interfaces is fully implemented via `@injectable(provide="Token")`. +* **Circular Dependency Detection**: **[RESOLVED]** + * *Actual:* The container tracks resolution paths and throws an explicit `CircularDependencyError` defined in `errors.py`. + +--- + +## 2. MCP Primitives (Tools, Resources, Prompts) +**Reference:** [02_mcp_primitives.md](file:///c:/Users/gauta/OneDrive/Desktop/nitrostack-codebases/docs/02_mcp_primitives.md) + +* **Omitting Description / Docstring Parsing**: **[RESOLVED]** + * *Actual:* The `@tool`, `@prompt`, and `@resource` decorators make the `description` argument optional and inspect function docstrings (`func.__doc__`) to extract the description if omitted. +* **Structured Validation Errors**: **[RESOLVED]** + * *Actual:* Validation errors (Pydantic and custom) are caught and translated into structured MCP protocol validation error blocks returning `types.CallToolResult(isError=True)`. + +--- + +## 3. Request Execution Pipeline +**Reference:** [03_execution_pipeline.md](file:///c:/Users/gauta/OneDrive/Desktop/nitrostack-codebases/docs/03_execution_pipeline.md) + +* **Middleware Signature / Lambda Crash**: **[RESOLVED]** + * *Actual:* Middleware execution chain is constructed using dynamic async steps that correctly support ASGI-like `call_next(context)` and parameterless `call_next()` invocations, propagating context changes downstream. +* **Guard Failure Handling**: **[RESOLVED]** + * *Actual:* Denials from guards (raising `PermissionError`) are caught by the translation layer and return an access denied result block with `isError=True`. + +--- + +## 4. Security & Authentication +**Reference:** [04_security_and_auth.md](file:///c:/Users/gauta/OneDrive/Desktop/nitrostack-codebases/docs/04_security_and_auth.md) + +* **Custom Cryptographic Implementations (Security Flaw - Outstanding)**: + * *Requirement:* The SDK must wrap standard, audited packages like `PyJWT`, `cryptography`, or `authlib` to manage tokens instead of writing custom cryptographic functions. + * *Actual:* In `nitrostack/auth/jwt.py`, signature creation and verification are implemented from scratch using Python's standard `hmac`, `hashlib`, and `base64` modules. This custom implementation only supports HS256, lacks compliance checks, is prone to cryptographic vulnerabilities, and does not support standard production key validation methods. +* **Missing Dependencies for JWKS (Missing / Packaging Defect - Outstanding)**: + * *Requirement:* Use standard Python packages for JWKS and signature checking. + * *Actual:* In `nitrostack/auth/oauth.py`, the JWKS verification code tries to import `jwt` (PyJWT) and use `jwt.PyJWKClient`. However, neither `PyJWT` nor `cryptography` are listed as dependencies in `pyproject.toml` or `requirements.txt`. If JWKS is enabled, invoking a protected endpoint results in a `ModuleNotFoundError`. +* **Asynchronous HTTP Client for OAuth Introspection (Missing - Outstanding)**: + * *Requirement:* Validation of authorization tokens against remote Discovery endpoints must use asynchronous HTTP clients like `httpx` instead of blocking synchronous HTTP clients. + * *Actual:* In `nitrostack/auth/oauth.py`, `introspect_token` constructs a request and calls the synchronous `urllib.request.urlopen` standard library utility. Even though it is run within an executor (`loop.run_in_executor`), it does not use a non-blocking asynchronous library like `httpx` as required. `httpx` is not specified as a project dependency. + +--- + +## 5. Async Background Tasks +**Reference:** [05_task_management.md](file:///c:/Users/gauta/OneDrive/Desktop/nitrostack-codebases/docs/05_task_management.md) + +* **Task Cancellation & `asyncio.CancelledError` (Bug - Outstanding)**: + * *Requirement:* If a client requests cancellation, `asyncio.CancelledError` is raised inside the running task, triggering standard cleanups and context manager releases. + * *Actual:* In `nitrostack/core/task.py`, the `cancel_task` method only updates the internal metadata state (`entry.is_cancelled = True`, `entry.status = "cancelled"`). The task execution runner does not keep a reference to the `asyncio.Task` object spawned by `asyncio.create_task()`, meaning it cannot cancel the running asyncio task. The background python code continues running to completion in the background, and no `asyncio.CancelledError` is ever raised. +* **Synchronous Blocking Operations Support (Missing - Outstanding)**: + * *Requirement:* Direct support for running synchronous tool functions in a separate thread pool executor (e.g. via `asyncio.to_thread`) to avoid event loop starvation. + * *Actual:* In `pipeline.py`'s `run_pipeline`, the handler is always directly awaited: `await handler(...)`. If a synchronous (blocking) function is registered as a tool, this call fails with a `TypeError` because synchronous functions are not awaitable. + * Furthermore, the `@cache` and `@rate_limit` decorators in `additional_decorators.py` are hardcoded to be asynchronous and explicitly await the wrapped function (`await func(...)`), which fails if they wrap a synchronous function. + +--- + +## 6. CLI Scaffolding & Testing Harness +**Reference:** [06_cli_and_testing.md](file:///c:/Users/gauta/OneDrive/Desktop/nitrostack-codebases/docs/06_cli_and_testing.md) + +* **Hardcoded CLI Target in `nitrostack-py dev`**: **[RESOLVED]** + * *Actual:* CLI subcommands correctly accept a custom `--file` argument, defaulting to `"main.py"`. +* **Missing Out-of-the-Box Pytest Fixture (Missing - Outstanding)**: + * *Requirement:* Provide a pre-configured `pytest` fixture (`nitro_client`) that instantiates an in-process mock transport. + * *Actual:* The SDK only exports the `NitroTestingModule` class. No pytest plugins, hooks, or fixtures (`nitro_client`) are defined or packaged by the SDK. + +--- + +## 7. UI Widgets & Next.js Adapters +**Reference:** [07_widgets_integration.md](file:///c:/Users/gauta/OneDrive/Desktop/nitrostack-codebases/docs/07_widgets_integration.md) + +* **Next.js Static HTML Inlining (Missing - Outstanding)**: + * *Requirement:* Compile a Next.js directory into a single HTML bundle (inline CSS/JS) using `compile_next_widget`. + * *Actual:* The entire frontend adaptation and compilation pipeline is missing. There is no `ui_next` module or `compile_next_widget` helper in the codebase. diff --git a/nitrostack/cli/main.py b/nitrostack/cli/main.py index d673dfe..dea2e2c 100644 --- a/nitrostack/cli/main.py +++ b/nitrostack/cli/main.py @@ -841,10 +841,16 @@ def print_banner(): ╚══════════════════════════════════════════════════════════╝\033[0m""" print(banner) -def init_project(name: str, template: str = None): +def init_project(name: str = None, template: str = None, description: str = None, author: str = None, skip_install: bool = False): print_banner() - # 1. Overwrite check + # 1. Prompt for project name if not provided + if not name: + sys.stdout.write("\033[32m? \033[1;37mProject name:\033[0m [my-mcp-server]: ") + sys.stdout.flush() + name = sys.stdin.readline().strip() or "my-mcp-server" + + # Overwrite check if os.path.exists(name): sys.stdout.write(f"\033[32m? \033[1;37mDirectory '{name}' already exists. Overwrite?\033[0m (Yes/No) [No]: ") sys.stdout.flush() @@ -887,13 +893,15 @@ def init_project(name: str, template: str = None): template = "flight-booking" # 3. Description and Author - sys.stdout.write("\033[32m? \033[1;37mDescription:\033[0m [My awesome MCP server]: ") - sys.stdout.flush() - description = sys.stdin.readline().strip() or "My awesome MCP server" + if description is None: + sys.stdout.write("\033[32m? \033[1;37mDescription:\033[0m [My awesome MCP server]: ") + sys.stdout.flush() + description = sys.stdin.readline().strip() or "My awesome MCP server" - sys.stdout.write("\033[32m? \033[1;37mAuthor:\033[0m [developer]: ") - sys.stdout.flush() - author = sys.stdin.readline().strip() or "developer" + if author is None: + sys.stdout.write("\033[32m? \033[1;37mAuthor:\033[0m [developer]: ") + sys.stdout.flush() + author = sys.stdin.readline().strip() or "developer" # 4. Copy template directory import nitrostack @@ -907,7 +915,6 @@ def init_project(name: str, template: str = None): shutil.copytree(template_src_dir, name) print("\n\033[32m✓\033[0m Project created") - print("\033[32m✓\033[0m Dependencies installed") # 5. Update .env file env_path = os.path.join(name, ".env") @@ -945,16 +952,19 @@ def init_project(name: str, template: str = None): pass # 7. Run npm install inside widgets directory - widgets_dir = os.path.join(name, "src", "widgets") - if os.path.exists(widgets_dir): - print("Installing widget dependencies...") - try: - subprocess.run(["npm", "--version"], shell=True, capture_output=True, check=True) - subprocess.run(["npm", "install"], cwd=widgets_dir, shell=True, check=True) - print("\033[32m✓\033[0m Widget dependencies installed\n") - except Exception as e: - print(f"Warning: Failed to install widget dependencies: {e}") - print("Please run 'npm install' inside 'src/widgets' manually.\n") + if not skip_install: + widgets_dir = os.path.join(name, "src", "widgets") + if os.path.exists(widgets_dir): + print("Installing widget dependencies...") + try: + subprocess.run(["npm", "--version"], shell=True, capture_output=True, check=True) + subprocess.run(["npm", "install"], cwd=widgets_dir, shell=True, check=True) + print("\033[32m✓\033[0m Widget dependencies installed\n") + except Exception as e: + print(f"Warning: Failed to install widget dependencies: {e}") + print("Please run 'npm install' inside 'src/widgets' manually.\n") + else: + print("Skipping dependencies installation (--skip-install)\n") # Success Card abs_path = os.path.abspath(name) @@ -975,14 +985,17 @@ def init_project(name: str, template: str = None): print(" See \033[34mOAUTH_SETUP.md\033[0m for provider guides") else: print(" 2. Configure environment variables in \033[34m.env\033[0m") - print(" 3. Start development server: \033[34mnitrostack-py dev\033[0m (or `python -m nitrostack.cli.main dev`)") - print(" 4. Start NitroStudio dashboard: \033[34mnitrostack-studio\033[0m (or `python -m nitrostack.studio`)") + if skip_install: + print(" 3. Install dependencies: \033[34mnitrostack-py install\033[0m") + print(" 4. Start development server: \033[34mnitrostack-py dev\033[0m") + else: + print(" 3. Start development server: \033[34mnitrostack-py dev\033[0m (or `python -m nitrostack.cli.main dev`)") + print(" 4. Start NitroStudio dashboard: \033[34mnitrostack-studio\033[0m (or `python -m nitrostack.studio`)") print("\nHappy coding! 🚀\n") -def run_dev(): - target = "main.py" +def run_dev(target="main.py", port=3001): if not os.path.exists(target): - print("Error: main.py not found in current directory.") + print(f"Error: {target} not found in current directory.") sys.exit(1) print(f"Starting hot-reload development server for {target}...") @@ -992,11 +1005,11 @@ def run_dev(): # Check if Next.js widgets are present widgets_dir = os.path.join(os.getcwd(), "src", "widgets") if os.path.exists(widgets_dir) and os.path.exists(os.path.join(widgets_dir, "package.json")): - print("Starting widget development server on port 3001...") + print(f"Starting widget development server on port {port}...") try: - # Spawn npm run dev -- --port 3001 + # Spawn npm run dev -- --port widgets_process = subprocess.Popen( - ["npm", "run", "dev", "--", "--port", "3001"], + ["npm", "run", "dev", "--", "--port", str(port)], cwd=widgets_dir, shell=True ) @@ -1101,41 +1114,520 @@ def get_mtimes(): print("\nStopping development server...") cleanup() -def run_start(): - target = "main.py" +def run_start(target="main.py", port=None): if not os.path.exists(target): - print("Error: main.py not found in current directory.") + print(f"Error: {target} not found in current directory.") sys.exit(1) print(f"Starting production server for {target}...") env = os.environ.copy() env["PYTHONPATH"] = os.path.abspath(".") + if port is not None: + env["PORT"] = str(port) try: subprocess.run([sys.executable, target], env=env) except KeyboardInterrupt: print("\nStopping server...") -def generate_tool(name: str): - filename = f"{name}_tool.py" - if os.path.exists(filename): - print(f"Error: File '{filename}' already exists.") +def run_install(skip_widgets=False, production=False): + print("Installing dependencies...") + # 1. Install Python dependencies in current directory + if os.path.exists("requirements.txt"): + print("Installing Python dependencies from requirements.txt...") + try: + subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], check=True) + print("\033[32m✓\033[0m Python dependencies installed") + except Exception as e: + print(f"Error: Failed to install Python dependencies: {e}") + elif os.path.exists("pyproject.toml"): + print("Installing Python dependencies from pyproject.toml...") + try: + if os.path.exists("poetry.lock"): + subprocess.run(["poetry", "install"], shell=True, check=True) + else: + subprocess.run([sys.executable, "-m", "pip", "install", "."], check=True) + print("\033[32m✓\033[0m Python dependencies installed") + except Exception as e: + print(f"Error: Failed to install Python dependencies: {e}") + + # 2. Install widget dependencies + if not skip_widgets: + widgets_dir = os.path.join(os.getcwd(), "src", "widgets") + if os.path.exists(widgets_dir) and os.path.exists(os.path.join(widgets_dir, "package.json")): + print("Installing widget dependencies...") + try: + cmd = ["npm", "install", "--production"] if production else ["npm", "install"] + subprocess.run(cmd, cwd=widgets_dir, shell=True, check=True) + print("\033[32m✓\033[0m Widget dependencies installed") + except Exception as e: + print(f"Error: Failed to install widget dependencies: {e}") + +def run_build(output="dist"): + print("Building project...") + widgets_dir = os.path.join(os.getcwd(), "src", "widgets") + if os.path.exists(widgets_dir) and os.path.exists(os.path.join(widgets_dir, "package.json")): + print("Building Next.js widgets...") + try: + subprocess.run(["npm", "run", "build"], cwd=widgets_dir, shell=True, check=True) + print("\033[32m✓\033[0m Widgets built successfully") + except Exception as e: + print(f"Error building widgets: {e}") + sys.exit(1) + else: + print("No widgets directory found to build.") + +def run_upgrade(dry_run=False, latest=False): + if dry_run: + print("Dry run: Checking for upgrades...") + print("Would run: pip install --upgrade nitrostack") + return + + print("Upgrading nitrostack...") + try: + cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "nitrostack"] + if latest: + cmd.append("--force-reinstall") + subprocess.run(cmd, check=True) + print("\033[32m✓\033[0m nitrostack upgraded successfully") + except Exception as e: + print(f"Error upgrading nitrostack: {e}") sys.exit(1) - camel_name = "".join(part.capitalize() for part in name.split("_")) - content = TOOL_TEMPLATE.format(name=name, camel_name=camel_name) - with open(filename, "w", encoding="utf-8") as f: - f.write(content) - print(f"Generated tool boilerplate in '{filename}'") -def generate_module(name: str): - filename = f"{name}_module.py" - if os.path.exists(filename): - print(f"Error: File '{filename}' already exists.") +# Generator boilerplate templates mapping +TEMPLATES = { + "middleware": """from nitrostack import ExecutionContext +from typing import Callable, Any + +class {camel_name}: + async def use(self, context: ExecutionContext, next_fn: Callable[[], Any]) -> Any: + # Use context.logger instead of print — print breaks JSON-RPC over stdio in MCP mode + context.logger.info(f"[{context.tool_name}] Started") + + result = await next_fn() + + context.logger.info(f"[{context.tool_name}] Completed") + return result +""", + "interceptor": """from nitrostack import ExecutionContext +from typing import Callable, Any +from datetime import datetime + +class {camel_name}: + async def intercept(self, context: ExecutionContext, next_fn: Callable[[], Any]) -> Any: + result = await next_fn() + + # Transform response + return {{ + "success": True, + "data": result, + "timestamp": datetime.utcnow().isoformat() + }} +""", + "pipe": """from nitrostack.core.pipeline import PipeMetadata +from typing import Any + +class {camel_name}: + async def transform(self, value: Any, metadata: PipeMetadata) -> Any: + # Validate + if not value: + raise ValueError("Value is required") + + # Transform + return value +""", + "filter": """from nitrostack import ExecutionContext +from datetime import datetime + +class {camel_name}: + async def catch(self, error: Exception, context: ExecutionContext): + error_message = str(error) + context.logger.error("Exception caught", extra={{ + "tool": context.tool_name, + "error": error_message + }}) + + return {{ + "error": True, + "message": error_message, + "timestamp": datetime.utcnow().isoformat() + }} +""", + "service": """from nitrostack import injectable + +@injectable() +class {camel_name}: + # Add your service methods here + + async def find_one(self, id: str) -> dict: + # TODO: Implement + return {{"id": id}} + + async def find_all(self) -> list: + # TODO: Implement + return [] + + async def create(self, data: dict) -> dict: + # TODO: Implement + return {{"id": "1", **data}} + + async def update(self, id: str, data: dict) -> dict: + # TODO: Implement + return {{"id": id, **data}} + + async def delete(self, id: str) -> None: + # TODO: Implement + pass +""", + "guard": """from nitrostack import ExecutionContext + +class {camel_name}: + async def can_activate(self, context: ExecutionContext) -> bool: + # TODO: Implement your guard logic + + # Example: Check if user has required role + user_role = getattr(context.auth, "role", None) + + if not user_role: + raise ValueError("Authentication required") + + if user_role != "admin": + raise ValueError("Insufficient permissions") + + return True +""", + "health": """import time +from nitrostack import health_check + +class {camel_name}: + def __init__(self): + self.start_time = time.time() + + @health_check("{name}") + def check_health(self) -> bool: + # TODO: Implement health check logic + uptime = time.time() - self.start_time + return uptime >= 0 +""", + "module": """from nitrostack import module +from modules.{name}.{name}_tools import {camel_name}Tools +from modules.{name}.{name}_resources import {camel_name}Resources +from modules.{name}.{name}_prompts import {camel_name}Prompts + +@module( + name="{name}", + controllers=[{camel_name}Tools, {camel_name}Resources, {camel_name}Prompts], + providers=[], + exports=[] +) +class {camel_name}Module: + pass +""", + "tools": """from nitrostack import injectable, tool, ExecutionContext +from pydantic import BaseModel, Field + +class {camel_name}ExampleInput(BaseModel): + id: str = Field(description="ID parameter") + +@injectable() +class {camel_name}Tools: + @tool( + name="{name}_example", + description="Implement your tool description here", + input_schema={camel_name}ExampleInput + ) + async def example_tool(self, input: {camel_name}ExampleInput, context: ExecutionContext): + # TODO: Implement tool logic + context.logger.info(f"Executing tool {name}_example") + return {{"id": input.id, "result": "success"}} +""", + "resources": """from nitrostack import injectable, resource, ExecutionContext + +@injectable() +class {camel_name}Resources: + @resource( + uri="{name}://example", + name="Example Resource", + description="Implement your resource description here", + mime_type="application/json" + ) + async def example_resource(self, context: ExecutionContext) -> dict: + # TODO: Implement resource logic + return {{"example": "data"}} +""", + "prompts": """from nitrostack import injectable, prompt, ExecutionContext + +@injectable() +class {camel_name}Prompts: + @prompt( + name="{name}-help", + description="Implement your prompt description here" + ) + async def help_prompt(self, args: dict, context: ExecutionContext) -> str: + # TODO: Implement prompt logic + return "You are an assistant. Help the user with resources." +""" +} + +def generate_component(type_name: str, name: str, options): + import re + + # Normalize type name + type_name = type_name.lower() + + # Check special case: tool/module compatibility + if type_name == "tool": + type_name = "tools" + + if type_name not in TEMPLATES: + print(f"Error: Invalid generator type '{type_name}'.") + print(f"Supported types: {', '.join(TEMPLATES.keys())}") + sys.exit(1) + + camel_name = "".join(part.capitalize() for part in name.replace("-", "_").split("_")) + snake_name = name.replace("-", "_").lower() + + # Get path mapping + base_path = os.getcwd() + +def generate_types(options): + import ast + import glob + + base_path = os.getcwd() + output_path = getattr(options, "output", None) + if not output_path: + widgets_types_dir = os.path.join(base_path, "src", "widgets", "src", "types") + if os.path.exists(widgets_types_dir): + output_path = os.path.join(widgets_types_dir, "generated-tools.ts") + else: + output_path = os.path.join(base_path, "src", "types", "generated-tools.ts") + + print("Scanning for tool definitions...") + + py_files = [] + for root, dirs, files in os.walk(base_path): + if any(p in root.split(os.sep) for p in ("venv", "env", "__pycache__", ".git", "src")): + continue + for file in files: + if file.endswith(".py") and file != "main.py": + py_files.append(os.path.join(root, file)) + + if not py_files: + print("No Python files found.") + return + + print(f"Found {len(py_files)} Python file(s).") + + tool_types = {} + + for file_path in py_files: + try: + with open(file_path, "r", encoding="utf-8") as f: + source = f.read() + + tree = ast.parse(source) + models = {} + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + inherits_base_model = False + for base in node.bases: + if isinstance(base, ast.Name) and base.id == "BaseModel": + inherits_base_model = True + elif isinstance(base, ast.Attribute) and base.attr == "BaseModel": + inherits_base_model = True + + if inherits_base_model: + fields = {} + for item in node.body: + if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name): + field_name = item.target.id + field_type = "any" + type_map = { + "str": "string", + "int": "number", + "float": "number", + "bool": "boolean", + "dict": "Record", + "list": "any[]", + "Any": "any" + } + if isinstance(item.annotation, ast.Name): + field_type = type_map.get(item.annotation.id, "any") + elif isinstance(item.annotation, ast.Subscript) and isinstance(item.annotation.value, ast.Name): + outer_type = item.annotation.value.id + if outer_type == "Optional": + if isinstance(item.annotation.slice, ast.Name): + field_type = type_map.get(item.annotation.slice.id, "any") + " | null" + elif outer_type in ("List", "list"): + field_type = "any[]" + elif outer_type in ("Dict", "dict"): + field_type = "Record" + fields[field_name] = field_type + models[node.name] = fields + + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + for decorator in node.decorator_list: + is_tool = False + tool_name = None + input_schema_name = None + + if isinstance(decorator, ast.Call): + if isinstance(decorator.func, ast.Name) and decorator.func.id == "tool": + is_tool = True + elif isinstance(decorator.func, ast.Attribute) and decorator.func.attr == "tool": + is_tool = True + + if is_tool: + for kw in decorator.keywords: + if kw.arg == "name" and isinstance(kw.value, ast.Constant): + tool_name = kw.value.value + elif kw.arg == "input_schema" and isinstance(kw.value, ast.Name): + input_schema_name = kw.value.id + + if is_tool and tool_name: + fields = models.get(input_schema_name, {}) + tool_types[tool_name] = fields + except Exception: + pass + + ts_output = f"""/** + * Auto-generated TypeScript types from NitroStack Python tool definitions + * DO NOT EDIT THIS FILE MANUALLY + * + * Generated by: nitrostack-py generate types + * Generated at: {time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} + */ + +export interface ToolInput {{ + [key: string]: T; +}} + +export interface ToolOutput {{ + [key: string]: T; +}} + +""" + for tool_name, fields in tool_types.items(): + pascal_name = "".join(w.capitalize() for w in tool_name.replace("-", "_").split("_")) + ts_output += f"// {tool_name}\n" + ts_output += f"export interface {pascal_name}Input {{\n" + for f_name, f_type in fields.items(): + ts_output += f" {f_name}: {f_type};\n" + if not fields: + ts_output += " [key: string]: any;\n" + ts_output += "}\n\n" + + if tool_types: + ts_output += f"export type ToolName = { ' | '.join(f'\"{name}\"' for name in tool_types.keys()) };\n\n" + else: + ts_output += "export type ToolName = string;\n\n" + + ts_output += "export interface ToolInputs {\n" + for tool_name in tool_types.keys(): + pascal_name = "".join(w.capitalize() for w in tool_name.replace("-", "_").split("_")) + ts_output += f" '{tool_name}': {pascal_name}Input;\n" + ts_output += "}\n" + + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + f.write(ts_output) + + print(f"\033[32m✓\033[0m Types generated successfully at: {os.path.relpath(output_path, base_path)}") + +def generate_component(type_name: str, name: str, options): + import re + + # Normalize type name + type_name = type_name.lower() + + # Special case for types generation + if type_name == "types": + generate_types(options) + return + + # Check special case: tool/module compatibility + if type_name == "tool": + type_name = "tools" + + if type_name not in TEMPLATES: + print(f"Error: Invalid generator type '{type_name}'.") + print(f"Supported types: {', '.join(TEMPLATES.keys())}, types") + sys.exit(1) + + camel_name = "".join(part.capitalize() for part in name.replace("-", "_").split("_")) + snake_name = name.replace("-", "_").lower() + + # Get path mapping + base_path = os.getcwd() + + # Module specific generation + if type_name in ("module", "tools", "resources", "prompts"): + module_name = getattr(options, "module", None) or snake_name + module_name_snake = module_name.replace("-", "_").lower() + + # Tools/Resources/Prompts should use suffix + suffix_map = { + "module": "module", + "tools": "tools", + "resources": "resources", + "prompts": "prompts" + } + suffix = suffix_map[type_name] + filename = f"{module_name_snake}_{suffix}.py" + + # Put inside modules/ + dir_path = os.path.join(base_path, "modules", module_name_snake) + file_path = os.path.join(dir_path, filename) + else: + # Other types go to their respective root directories + dir_map = { + "middleware": "middleware", + "interceptor": "interceptors", + "pipe": "pipes", + "filter": "filters", + "service": "services", + "guard": "guards", + "health": "health" + } + dir_name = dir_map[type_name] + filename = f"{snake_name}.py" + dir_path = os.path.join(base_path, dir_name) + file_path = os.path.join(dir_path, filename) + + # Custom output path override + if getattr(options, "output", None): + file_path = os.path.abspath(options.output) + dir_path = os.path.dirname(file_path) + + # Create dir if not exists + if not os.path.exists(dir_path): + os.makedirs(dir_path, exist_ok=True) + + # Check if exists and force option + if os.path.exists(file_path) and not getattr(options, "force", False): + print(f"Error: File '{file_path}' already exists. Use --force to overwrite.") sys.exit(1) - camel_name = "".join(part.capitalize() for part in name.split("_")) - content = MODULE_TEMPLATE.format(name=name, camel_name=camel_name) - with open(filename, "w", encoding="utf-8") as f: + + # Build template + content = TEMPLATES[type_name].format(name=snake_name, camel_name=camel_name) + with open(file_path, "w", encoding="utf-8") as f: f.write(content) - print(f"Generated module boilerplate in '{filename}'") + + print(f"\033[32m✓\033[0m Created {type_name}: {os.path.relpath(file_path, base_path)}") + + # If generating module, generate related tools, resources, prompts unless skip-related is set + if type_name == "module" and not getattr(options, "skip_related", False): + for sub_type in ("tools", "resources", "prompts"): + sub_filename = f"{snake_name}_{sub_type}.py" + sub_file_path = os.path.join(dir_path, sub_filename) + if os.path.exists(sub_file_path) and not getattr(options, "force", False): + continue + + sub_content = TEMPLATES[sub_type].format(name=snake_name, camel_name=camel_name) + with open(sub_file_path, "w", encoding="utf-8") as f: + f.write(sub_content) + print(f"\033[32m✓\033[0m Created {sub_type}: {os.path.relpath(sub_file_path, base_path)}") def get_claude_config_paths(): paths = [] @@ -1238,14 +1730,35 @@ def main(): # init command init_parser = subparsers.add_parser("init", help="Initialize a new NitroStack MCP server project") - init_parser.add_argument("name", help="Name of the project directory to create") + init_parser.add_argument("name", nargs="?", default=None, help="Name of the project directory to create") init_parser.add_argument("--template", choices=["calculator", "food-delivery", "flight-booking", "starter", "pizzaz", "oauth"], default=None, help="Template to use (default: interactive prompt)") + init_parser.add_argument("--description", default=None, help="Description of the project") + init_parser.add_argument("--author", default=None, help="Author of the project") + init_parser.add_argument("--skip-install", action="store_true", help="Skip installing dependencies") # dev command - subparsers.add_parser("dev", help="Start the hot-reloading development server") + dev_parser = subparsers.add_parser("dev", help="Start the hot-reloading development server") + dev_parser.add_argument("--file", default="main.py", help="Python script to register (defaults to main.py)") + dev_parser.add_argument("--port", type=int, default=3001, help="Port for widget dev server (default: 3001)") # start command - subparsers.add_parser("start", help="Start the production server") + start_parser = subparsers.add_parser("start", help="Start the production server") + start_parser.add_argument("--file", default="main.py", help="Python script to register (defaults to main.py)") + start_parser.add_argument("--port", type=int, default=None, help="Port for server") + + # install command + install_parser = subparsers.add_parser("install", aliases=["i"], help="Install dependencies in root and src/widgets directories") + install_parser.add_argument("--skip-widgets", action="store_true", help="Skip installing widget dependencies") + install_parser.add_argument("--production", action="store_true", help="Install production dependencies only") + + # build command + build_parser = subparsers.add_parser("build", help="Build the project for production") + build_parser.add_argument("--output", default="dist", help="Output directory") + + # upgrade command + upgrade_parser = subparsers.add_parser("upgrade", help="Upgrade nitrostack to the latest version in the project") + upgrade_parser.add_argument("--dry-run", action="store_true", help="Show what would be upgraded without making changes") + upgrade_parser.add_argument("--latest", action="store_true", help="Force upgrade to the latest version even if already up to date") # register command reg_parser = subparsers.add_parser("register", help="Register server script inside Claude Desktop configuration") @@ -1253,14 +1766,22 @@ def main(): reg_parser.add_argument("--file", default="main.py", help="Python script to register (defaults to main.py)") # generate command - gen_parser = subparsers.add_parser("generate", help="Generate boilerplate code") + gen_parser = subparsers.add_parser("generate", aliases=["g"], help="Generate boilerplate code") + gen_parser.add_argument("--module", help="Module name (for module-specific generation)") + gen_parser.add_argument("--output", help="Output path") + gen_parser.add_argument("--force", action="store_true", help="Overwrite existing files") + gen_parser.add_argument("--skip-related", action="store_true", help="Skip generating related files (for modules)") gen_subparsers = gen_parser.add_subparsers(dest="generator") - tool_parser = gen_subparsers.add_parser("tool", help="Generate a new tool boilerplate") - tool_parser.add_argument("name", help="Name of the tool") - - mod_parser = gen_subparsers.add_parser("module", help="Generate a new module boilerplate") - mod_parser.add_argument("name", help="Name of the module") + # types subparser (without name argument) + gen_subparsers.add_parser("types", help="Generate TypeScript definitions from tool definitions") + + # Generator subcommands (all except types require name) + for gen_type in list(TEMPLATES.keys()) + ["tool"]: + if gen_type == "types": + continue + gen_type_parser = gen_subparsers.add_parser(gen_type, help=f"Generate a new {gen_type} boilerplate") + gen_type_parser.add_argument("name", help=f"Name of the {gen_type}") args = parser.parse_args() @@ -1269,21 +1790,25 @@ def main(): sys.exit(1) if args.command == "init": - init_project(args.name, args.template) + init_project(args.name, args.template, args.description, args.author, args.skip_install) elif args.command == "dev": - run_dev() + run_dev(args.file, args.port) elif args.command == "start": - run_start() + run_start(args.file, args.port) + elif args.command == "install": + run_install(args.skip_widgets, args.production) + elif args.command == "build": + run_build(args.output) + elif args.command == "upgrade": + run_upgrade(args.dry_run, args.latest) elif args.command == "register": register_server(args.name, args.file) elif args.command == "generate": if not args.generator: parser.parse_args(["generate", "--help"]) sys.exit(1) - if args.generator == "tool": - generate_tool(args.name) - elif args.generator == "module": - generate_module(args.name) + name = getattr(args, "name", None) + generate_component(args.generator, name, args) if __name__ == "__main__": main() diff --git a/nitrostack/core/additional_decorators.py b/nitrostack/core/additional_decorators.py index 14df053..15978af 100644 --- a/nitrostack/core/additional_decorators.py +++ b/nitrostack/core/additional_decorators.py @@ -1,6 +1,9 @@ import time +import re +import json +import threading from functools import wraps -from typing import Callable, Any, Dict, List, Tuple +from typing import Callable, Any, Dict, List, Tuple, Union, Optional from nitrostack.core.context import ExecutionContext def copy_mcp_attributes(src: Any, dst: Any) -> None: @@ -9,23 +12,94 @@ def copy_mcp_attributes(src: Any, dst: Any) -> None: if attr.startswith("_mcp_"): setattr(dst, attr, getattr(src, attr)) -def cache(ttl: int = 60): +class InMemoryCacheStorage: + def __init__(self): + self._cache = {} + self._lock = threading.Lock() + + def get(self, key: str) -> Any: + with self._lock: + item = self._cache.get(key) + if not item: + return None + if item["expires"] < time.time(): + self._cache.pop(key, None) + return None + return item["value"] + + def set(self, key: str, value: Any, ttl: int) -> None: + with self._lock: + self._cache[key] = { + "value": value, + "expires": time.time() + ttl + } + + def delete(self, key: str) -> None: + with self._lock: + self._cache.pop(key, None) + + def clear(self) -> None: + with self._lock: + self._cache.clear() + + def cleanup(self) -> None: + with self._lock: + now = time.time() + for key, item in list(self._cache.items()): + if item["expires"] < now: + self._cache.pop(key, None) + +default_cache_storage = InMemoryCacheStorage() + +def _start_cache_cleanup(): + def cleanup_loop(): + while True: + time.sleep(60) + try: + default_cache_storage.cleanup() + except Exception: + pass + t = threading.Thread(target=cleanup_loop, daemon=True) + t.start() + +_start_cache_cleanup() + +def cache( + ttl: int = 60, + key: Optional[Callable[[Any, Any], str]] = None, + storage: Any = None +): """ Caches method outputs for a specific TTL (in seconds). - Skips ExecutionContext parameters when generating the cache key. + Constructs a cache key by serializing inputs, excluding the ExecutionContext. """ - def decorator(func: Callable): - cache_store: Dict[Tuple[Any, ...], Tuple[Any, float]] = {} + active_storage = storage or default_cache_storage + def decorator(func: Callable): @wraps(func) async def wrapper(*args, **kwargs): - # Construct a cache key by filtering out ExecutionContext and self + # Find execution context + context = None + for arg in args: + if isinstance(arg, ExecutionContext) or type(arg).__name__ == "ExecutionContext": + context = arg + break + if not context: + for v in kwargs.values(): + if isinstance(v, ExecutionContext) or type(v).__name__ == "ExecutionContext": + context = v + break + + # Filter args and kwargs for the cache key filtered_args = [] for arg in args: - # Skip self if it has class attributes or is the instance, skip ExecutionContext if isinstance(arg, ExecutionContext) or type(arg).__name__ == "ExecutionContext": continue - filtered_args.append(arg) + # If arg is 'self', use its class name + if hasattr(arg, "__class__") and not isinstance(arg, (int, float, str, bool, list, dict, set, tuple)) and arg.__class__.__name__ not in ("int", "float", "str", "bool", "list", "dict", "set", "tuple"): + filtered_args.append(arg.__class__.__name__) + else: + filtered_args.append(arg) filtered_kwargs = {} for k, v in kwargs.items(): @@ -33,40 +107,170 @@ async def wrapper(*args, **kwargs): continue filtered_kwargs[k] = v - # Standardize key (args and kwargs) - key = (tuple(filtered_args), frozenset(filtered_kwargs.items())) + # Generate cache key + if key: + input_val = args[0] if len(args) > 0 else None + # Strip self if present + if len(args) > 1 and hasattr(args[0], "__class__") and args[0].__class__.__name__ not in ("int", "float", "str", "bool", "list", "dict", "set", "tuple"): + input_val = args[1] + cache_key = key(input_val, context) + else: + serialized_args = [] + for a in filtered_args: + if isinstance(a, dict) and "_meta" in a: + a_copy = a.copy() + a_copy.pop("_meta") + serialized_args.append(a_copy) + else: + serialized_args.append(a) + + serialized_kwargs = {} + for k, v in filtered_kwargs.items(): + if k == "_meta": + continue + serialized_kwargs[k] = v + + try: + key_parts = { + "func": f"{func.__module__}.{func.__name__}", + "args": serialized_args, + "kwargs": serialized_kwargs + } + cache_key = json.dumps(key_parts, sort_keys=True) + except Exception: + cache_key = f"{func.__module__}.{func.__name__}:{str(filtered_args)}:{str(filtered_kwargs)}" - now = time.time() - if key in cache_store: - val, expiry = cache_store[key] - if now < expiry: - return val + import sys + print(f"[Cache DEBUG] Checking cache for key: {cache_key}", file=sys.stderr) + + # Check cache + cached_val = active_storage.get(cache_key) + if cached_val is not None: + print(f"[Cache DEBUG] HIT - returning cached result", file=sys.stderr) + if context and getattr(context, "logger", None): + context.logger.info(f"[Cache] Hit for key: {cache_key}") + return cached_val + print(f"[Cache DEBUG] MISS - executing method", file=sys.stderr) result = await func(*args, **kwargs) - cache_store[key] = (result, now + ttl) + + # Store in cache + active_storage.set(cache_key, result, ttl) + print(f"[Cache DEBUG] Stored result for {ttl}s", file=sys.stderr) + if context and getattr(context, "logger", None): + context.logger.info(f"[Cache] Miss for key: {cache_key}, stored for {ttl}s") + return result copy_mcp_attributes(func, wrapper) return wrapper return decorator -def rate_limit(max: int, window: int): +def parse_window(window: Union[int, str]) -> int: + """Parse time window string (e.g. '1m', '10s', '2h') to seconds.""" + if isinstance(window, int): + return window + match = re.match(r"^(\d+)([smhd])$", window) + if not match: + raise ValueError(f"Invalid time window format: {window}. Use format like '1m', '1h', '1d'") + value = int(match[1]) + unit = match[2] + multipliers = { + "s": 1, + "m": 60, + "h": 3600, + "d": 86400, + } + return value * multipliers[unit] + +class InMemoryRateLimitStorage: + def __init__(self): + self._limits = {} + self._lock = threading.Lock() + + def increment(self, key: str, window_seconds: int) -> int: + with self._lock: + now = time.time() + limit = self._limits.get(key) + if not limit or limit["reset_at"] < now: + self._limits[key] = { + "count": 1, + "reset_at": now + window_seconds + } + return 1 + limit["count"] += 1 + return limit["count"] + + def reset(self, key: str) -> None: + with self._lock: + self._limits.pop(key, None) + + def cleanup(self) -> None: + with self._lock: + now = time.time() + for key, limit in list(self._limits.items()): + if limit["reset_at"] < now: + self._limits.pop(key, None) + +default_rate_limit_storage = InMemoryRateLimitStorage() + +def _start_rate_limit_cleanup(): + def cleanup_loop(): + while True: + time.sleep(60) + try: + default_rate_limit_storage.cleanup() + except Exception: + pass + t = threading.Thread(target=cleanup_loop, daemon=True) + t.start() + +_start_rate_limit_cleanup() + +def rate_limit( + max: int, + window: Union[int, str], + key: Optional[Callable[[ExecutionContext], str]] = None, + storage: Any = None, + message: Optional[str] = None +): """ - Rate limits method calls to max calls per window (in seconds). + Rate limits method calls to max calls per window. """ - def decorator(func: Callable): - calls: List[float] = [] + window_secs = parse_window(window) + active_storage = storage or default_rate_limit_storage + def decorator(func: Callable): @wraps(func) async def wrapper(*args, **kwargs): - nonlocal calls - now = time.time() - # Filter timestamps in the window - calls = [t for t in calls if now - t < window] - if len(calls) >= max: - raise ValueError(f"Rate limit exceeded. Maximum of {max} calls allowed every {window} seconds.") - - calls.append(now) + context = None + for arg in args: + if isinstance(arg, ExecutionContext) or type(arg).__name__ == "ExecutionContext": + context = arg + break + if not context: + for v in kwargs.values(): + if isinstance(v, ExecutionContext) or type(v).__name__ == "ExecutionContext": + context = v + break + + if key and context: + rate_key = key(context) + else: + rate_key = getattr(getattr(context, "auth", None), "subject", None) or "anonymous" + + full_key = f"{func.__module__}.{func.__name__}:{rate_key}" + count = active_storage.increment(full_key, window_secs) + + if count > max: + err_msg = message or f"Rate limit exceeded. Maximum {max} requests per {window}" + if context and getattr(context, "logger", None): + context.logger.warn(f"[RateLimit] Limit exceeded for key: {full_key}") + raise ValueError(err_msg) + + if context and getattr(context, "logger", None): + context.logger.info(f"[RateLimit] Request {count}/{max} for key: {full_key}") + return await func(*args, **kwargs) copy_mcp_attributes(func, wrapper) @@ -86,7 +290,6 @@ def register(cls, name: str, func: Callable, class_type: Any = None) -> None: @classmethod def bind_instance(cls, name: str, func: Callable, instance: Any) -> None: - # Bind the method to the resolved instance import inspect if inspect.ismethod(func): cls._bound_checks[name] = func @@ -102,10 +305,8 @@ def run_all(cls) -> Dict[str, str]: results = {} for name, check_fn in cls._bound_checks.items(): try: - # Handle both sync and async checks import inspect if inspect.iscoroutinefunction(check_fn): - # In a real environment, we'd await it, but let's handle it or run via loop import asyncio try: loop = asyncio.get_event_loop() @@ -126,8 +327,6 @@ def health_check(name: str): """ def decorator(func: Callable): func._mcp_health_check_name = name - # We don't have the class type here yet, but we will register it. - # It will be bound during discovery in the McpApplicationFactory. HealthCheckRegistry.register(name, func) return func return decorator diff --git a/nitrostack/core/app.py b/nitrostack/core/app.py index b694ab7..e7b3b87 100644 --- a/nitrostack/core/app.py +++ b/nitrostack/core/app.py @@ -18,18 +18,63 @@ # Monkeypatch FastMCP.call_tool to support returning CreateTaskResult without conversion _original_fastmcp_call_tool = FastMCP.call_tool +def handle_tool_error(e: Exception) -> Any: + import mcp.types as types + import pydantic + from nitrostack.core.errors import ValidationError + + # 1. Pydantic v2 ValidationError + if isinstance(e, pydantic.ValidationError): + errors_details = [] + for err in e.errors(): + loc = " -> ".join(str(l) for l in err.get("loc", [])) + msg = err.get("msg", "Unknown error") + errors_details.append(f"Field '{loc}': {msg}") + err_msg = "Validation failed:\n" + "\n".join(errors_details) + return types.CallToolResult( + content=[types.TextContent(type="text", text=err_msg)], + isError=True + ) + + # 2. SDK custom ValidationError + if isinstance(e, ValidationError): + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Validation Error: {str(e)}")], + isError=True + ) + + # 3. PermissionError / Guard auth failures + if isinstance(e, PermissionError) or "Access denied" in str(e): + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Access Denied: {str(e)}")], + isError=True + ) + + # 4. Fallback for other exceptions + return types.CallToolResult( + content=[types.TextContent(type="text", text=f"Error: {str(e)}")], + isError=True + ) + + async def _custom_fastmcp_call_tool(self, name: str, arguments: dict[str, Any]): t = self._tool_manager.get_tool(name) if not t: return await _original_fastmcp_call_tool(self, name, arguments) context = self.get_context() - result = await self._tool_manager.call_tool( - name, arguments, context=context, convert_result=False - ) - import mcp.types as types - if isinstance(result, types.CreateTaskResult): - return result - return t.fn_metadata.convert_result(result) + try: + result = await self._tool_manager.call_tool( + name, arguments, context=context, convert_result=False + ) + import mcp.types as types + if isinstance(result, types.CreateTaskResult): + return result + return t.fn_metadata.convert_result(result) + except Exception as e: + orig_err = e + if hasattr(e, "__cause__") and e.__cause__: + orig_err = e.__cause__ + return handle_tool_error(orig_err) FastMCP.call_tool = _custom_fastmcp_call_tool @@ -82,8 +127,6 @@ def get_pydantic_model(schema: Any) -> Type[BaseModel]: # Return a default empty model if invalid or empty return create_model("EmptyInputModel") - - class McpApplication: def __init__(self, app_class: Type): self.app_class = app_class @@ -161,6 +204,17 @@ def health_status_resource() -> str: results = HealthCheckRegistry.run_all() return json.dumps(results) + # 4b. Register Built-in Widget Examples Resource if widget-manifest.json exists + widget_manifest_path = os.path.join(os.getcwd(), "src", "widgets", "widget-manifest.json") + if os.path.exists(widget_manifest_path): + @self.mcp_server.resource("widget://examples", name="Widget Examples", description="Provides metadata and examples for all registered UI widgets", mime_type="application/json") + def widget_examples_resource() -> str: + try: + with open(widget_manifest_path, "r", encoding="utf-8") as f: + return f.read() + except Exception as e: + return f'{{"error": "Failed to read manifest: {str(e)}"}}' + # 5. Register Task Support hook & endpoints low_level_server = self.mcp_server._mcp_server original_get_caps = low_level_server.get_capabilities @@ -258,6 +312,8 @@ async def handle_get_task_payload(req): isError=True ) else: + if isinstance(t.error, types.CallToolResult): + return t.error return types.CallToolResult( content=[types.TextContent(type="text", text=str(t.error or t.status_message))], isError=True @@ -412,6 +468,11 @@ async def background_execution(): elif isinstance(result_dump, dict): if "content" in result_dump and "isError" in result_dump: final_result = types.CallToolResult(**result_dump) + elif "error" in result_dump: + final_result = types.CallToolResult( + content=[types.TextContent(type="text", text=str(result_dump.get("message") or result_dump.get("error")))], + isError=True + ) else: final_result = types.CallToolResult( content=[types.TextContent(type="text", text=json.dumps(result_dump, indent=2))], @@ -425,7 +486,8 @@ async def background_execution(): ) TaskRegistry.complete_task(task_id, final_result) except Exception as e: - TaskRegistry.fail_task(task_id, e) + translated_error = handle_tool_error(e) + TaskRegistry.fail_task(task_id, translated_error) import asyncio asyncio.create_task(background_execution()) @@ -459,24 +521,45 @@ async def background_execution(): filters = getattr(method, "_mcp_filters", []) # Run through pipeline runner - result = await run_pipeline( - handler=method, - handler_instance=instance, - args=(input, ctx), - kwargs={}, - context=ctx, - guards=guards, - middleware=middleware, - interceptors=interceptors, - pipes=pipes, - filters=filters, - param_name="input", - param_type=input_model - ) + try: + result = await run_pipeline( + handler=method, + handler_instance=instance, + args=(input, ctx), + kwargs={}, + context=ctx, + guards=guards, + middleware=middleware, + interceptors=interceptors, + pipes=pipes, + filters=filters, + param_name="input", + param_type=input_model + ) - if isinstance(result, BaseModel): - return result.model_dump() - return result + if isinstance(result, BaseModel): + result_dump = result.model_dump() + else: + result_dump = result + + import mcp.types as types + if isinstance(result_dump, types.CallToolResult): + return result_dump + elif isinstance(result_dump, dict): + if "content" in result_dump and "isError" in result_dump: + return types.CallToolResult(**result_dump) + elif "error" in result_dump: + err_msg = result_dump.get("message") or result_dump.get("error") + return types.CallToolResult( + content=[types.TextContent(type="text", text=str(err_msg))], + isError=True + ) + else: + return result_dump + else: + return result_dump + except Exception as e: + return handle_tool_error(e) # Register metadata meta = { diff --git a/nitrostack/core/decorators.py b/nitrostack/core/decorators.py index debdc84..c1400d7 100644 --- a/nitrostack/core/decorators.py +++ b/nitrostack/core/decorators.py @@ -71,8 +71,8 @@ class PromptConfig: def tool( name: str, - description: str, - input_schema: Any, + description: Optional[str] = None, + input_schema: Optional[Any] = None, title: Optional[str] = None, output_schema: Optional[Any] = None, annotations: Optional[ToolAnnotations] = None, @@ -91,11 +91,17 @@ def tool( metadata = {} def decorator(func: Callable): + desc = description + if not desc and func.__doc__: + desc = func.__doc__.strip() + if not desc: + desc = "" + # Attach or update tool config config = ToolConfig( name=name, title=title, - description=description, + description=desc, input_schema=input_schema, output_schema=output_schema, annotations=annotations, @@ -143,7 +149,7 @@ def initial_tool(func: Callable): def resource( uri: str, name: str, - description: str, + description: Optional[str] = None, title: Optional[str] = None, mime_type: Optional[str] = None, size: Optional[int] = None, @@ -159,10 +165,16 @@ def resource( metadata = {} def decorator(func: Callable): + desc = description + if not desc and func.__doc__: + desc = func.__doc__.strip() + if not desc: + desc = "" + config = ResourceConfig( uri=uri, name=name, - description=description, + description=desc, title=title, mime_type=mime_type, size=size, @@ -175,7 +187,7 @@ def decorator(func: Callable): def prompt( name: str, - description: str, + description: Optional[str] = None, arguments: Optional[List[PromptArgument]] = None, ): """ @@ -185,9 +197,15 @@ def prompt( arguments = [] def decorator(func: Callable): + desc = description + if not desc and func.__doc__: + desc = func.__doc__.strip() + if not desc: + desc = "" + config = PromptConfig( name=name, - description=description, + description=desc, arguments=arguments ) func._mcp_prompt_config = config diff --git a/nitrostack/core/pipeline.py b/nitrostack/core/pipeline.py index f529910..e64fc68 100644 --- a/nitrostack/core/pipeline.py +++ b/nitrostack/core/pipeline.py @@ -197,14 +197,25 @@ async def execute_handler_flow(): current_args[0] = val # 3. Chain Middleware and Interceptors - async def call_target(): + async def call_target(ctx): + target_args = list(current_args) + target_kwargs = dict(kwargs) + + for idx, arg in enumerate(target_args): + if isinstance(arg, ExecutionContext) or type(arg).__name__ == "ExecutionContext": + target_args[idx] = ctx + + for k, v in target_kwargs.items(): + if isinstance(v, ExecutionContext) or type(v).__name__ == "ExecutionContext": + target_kwargs[k] = ctx + import inspect if inspect.ismethod(handler): - return await handler(*current_args, **kwargs) + return await handler(*target_args, **target_kwargs) elif handler_instance is not None: - return await handler(handler_instance, *current_args, **kwargs) + return await handler(handler_instance, *target_args, **target_kwargs) else: - return await handler(*current_args, **kwargs) + return await handler(*target_args, **target_kwargs) # We construct the next chain backwards: # Middleware -> Interceptors -> Handler @@ -212,17 +223,27 @@ async def call_target(): for interceptor_cls in reversed(interceptors): interceptor = container.resolve(interceptor_cls) def make_interceptor_next(nxt): - return lambda: interceptor.intercept(context, nxt) + async def step(ctx): + async def call_next(*a, **k): + passed_ctx = a[0] if a else ctx + return await nxt(passed_ctx) + return await interceptor.intercept(ctx, call_next) + return step current_next = make_interceptor_next(current_next) for middleware_cls in reversed(middleware): mw = container.resolve(middleware_cls) def make_middleware_next(nxt): - return lambda: mw.use(context, nxt) + async def step(ctx): + async def call_next(*a, **k): + passed_ctx = a[0] if a else ctx + return await nxt(passed_ctx) + return await mw.use(ctx, call_next) + return step current_next = make_middleware_next(current_next) # Execute the chain - return await current_next() + return await current_next(context) # Wrap the entire flow in Exception Filters try: diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 0000000..ba3afb2 --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,185 @@ +import asyncio +import os +import sys +import time +from pydantic import BaseModel + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from nitrostack import ExecutionContext, AuthContext +from nitrostack.core.additional_decorators import cache, rate_limit + +class MockLogger: + def __init__(self): + self.infos = [] + self.warns = [] + + def info(self, msg): + self.infos.append(msg) + + def warn(self, msg): + self.warns.append(msg) + +async def _test_cache_decorator(): + call_count = 0 + + @cache(ttl=1) + async def get_value(x: int, context: ExecutionContext): + nonlocal call_count + call_count += 1 + return x * 2 + + ctx = ExecutionContext( + request_id="test-req-cache", + tool_name="test_tool", + metadata={}, + logger=MockLogger() + ) + + # 1. First call - miss + res1 = await get_value(10, ctx) + assert res1 == 20 + assert call_count == 1 + + # 2. Second call - hit + res2 = await get_value(10, ctx) + assert res2 == 20 + assert call_count == 1 # count should not increment + + # 3. Call with different parameter - miss + res3 = await get_value(20, ctx) + assert res3 == 40 + assert call_count == 2 + + # 4. Wait for TTL expiration + await asyncio.sleep(1.1) + res4 = await get_value(10, ctx) + assert res4 == 20 + assert call_count == 3 # count should increment now + +def test_cache_decorator(): + asyncio.run(_test_cache_decorator()) + +async def _test_rate_limit_decorator(): + call_count = 0 + + @rate_limit(max=2, window=1) + async def limited_function(context: ExecutionContext): + nonlocal call_count + call_count += 1 + return "ok" + + ctx_anon = ExecutionContext( + request_id="test-req-rl1", + tool_name="test_tool", + metadata={}, + logger=MockLogger(), + auth=AuthContext(subject="anon-user") + ) + + ctx_other = ExecutionContext( + request_id="test-req-rl2", + tool_name="test_tool", + metadata={}, + logger=MockLogger(), + auth=AuthContext(subject="other-user") + ) + + # User 'anon-user' calling twice: should pass + assert await limited_function(ctx_anon) == "ok" + assert await limited_function(ctx_anon) == "ok" + + # User 'anon-user' calling third time: should raise ValueError + try: + await limited_function(ctx_anon) + assert False, "Should have rate limited" + except ValueError as exc: + assert "rate limit exceeded" in str(exc).lower() + + # User 'other-user' calling: should pass (partitioned limits) + assert await limited_function(ctx_other) == "ok" + +def test_rate_limit_decorator(): + asyncio.run(_test_rate_limit_decorator()) + +async def _test_widget_examples_resource(): + from nitrostack.core.app import McpApplication, ServerConfig + from nitrostack import module + import tempfile + import shutil + import json + + # Create temp directory layout matching src/widgets/widget-manifest.json + temp_dir = tempfile.mkdtemp() + widgets_dir = os.path.join(temp_dir, "src", "widgets") + os.makedirs(widgets_dir) + + manifest_data = { + "version": "1.0.0", + "widgets": [ + {"uri": "widget://test", "name": "TestWidget", "description": "Desc", "examples": []} + ], + "generatedAt": "2026-07-07" + } + + manifest_path = os.path.join(widgets_dir, "widget-manifest.json") + with open(manifest_path, "w", encoding="utf-8") as f: + json.dump(manifest_data, f) + + old_cwd = os.getcwd() + os.chdir(temp_dir) + try: + @module(name="test_widget") + class TestWidgetModule: + pass + app = McpApplication(TestWidgetModule) + import mcp.types as types + assert "widget://examples" in app.mcp_server._resource_manager._resources + + handler = app.mcp_server._mcp_server.request_handlers[types.ReadResourceRequest] + req = types.ReadResourceRequest( + method="resources/read", + params=types.ReadResourceRequestParams(uri="widget://examples") + ) + res = await handler(req) + data = json.loads(res.root.contents[0].text) + assert data["version"] == "1.0.0" + assert data["widgets"][0]["name"] == "TestWidget" + finally: + os.chdir(old_cwd) + shutil.rmtree(temp_dir) + +def test_widget_examples_resource(): + asyncio.run(_test_widget_examples_resource()) + +async def _test_docstring_fallback(): + from nitrostack.core.decorators import tool, prompt, resource + + class DummyInput(BaseModel): + val: int + + @tool(name="doc_tool", input_schema=DummyInput) + def my_tool_func(input: DummyInput, ctx: ExecutionContext): + """This is my tool docstring description. + It spans multiple lines. + """ + pass + + assert my_tool_func._mcp_tool_config.description == "This is my tool docstring description.\n It spans multiple lines." + + @prompt(name="doc_prompt") + def my_prompt_func(args: dict, ctx: ExecutionContext): + """This is my prompt docstring.""" + pass + + assert my_prompt_func._mcp_prompt_config.description == "This is my prompt docstring." + + @resource(uri="doc://resource", name="doc_res") + def my_resource_func(ctx: ExecutionContext): + """This is my resource docstring.""" + pass + + assert my_resource_func._mcp_resource_config.description == "This is my resource docstring." + +def test_docstring_fallback(): + asyncio.run(_test_docstring_fallback()) diff --git a/tests/test_pipeline_gaps.py b/tests/test_pipeline_gaps.py new file mode 100644 index 0000000..843dfd5 --- /dev/null +++ b/tests/test_pipeline_gaps.py @@ -0,0 +1,73 @@ +import asyncio +import os +import sys +from pydantic import BaseModel + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from nitrostack import injectable, tool, use_guards, use_middleware, ExecutionContext, DIContainer +from nitrostack.core.pipeline import run_pipeline +from nitrostack.testing import NitroTestingModule +from nitrostack import module + +class DummyInput(BaseModel): + val: int + +class FailingGuard: + async def can_activate(self, context: ExecutionContext) -> bool: + return False + +class MyMiddleware: + async def use(self, context: ExecutionContext, call_next): + context.metadata["middleware_run"] = True + return await call_next(context) + +@injectable() +class PipelineController: + @tool(name="test_val_error", input_schema=DummyInput) + async def test_val_error(self, input: DummyInput, ctx: ExecutionContext) -> str: + return f"ok: {input.val}" + + @tool(name="test_guard_error", input_schema=DummyInput) + @use_guards(FailingGuard) + async def test_guard_error(self, input: DummyInput, ctx: ExecutionContext) -> str: + return "should not reach" + + @tool(name="test_mw_success", input_schema=DummyInput) + @use_middleware(MyMiddleware) + async def test_mw_success(self, input: DummyInput, ctx: ExecutionContext) -> str: + return f"mw_run: {ctx.metadata.get('middleware_run')}" + +@module( + name="pipeline_test", + controllers=[PipelineController], + providers=[FailingGuard, MyMiddleware] +) +class PipelineTestModule: + pass + +async def _test_pipeline_gaps(): + harness = await NitroTestingModule.create(PipelineTestModule) + + # 1. Test Pydantic validation error handling + # Passing invalid inputs (string instead of int, or missing fields) + import mcp.types as types + res_val = await harness.app.mcp_server.call_tool("test_val_error", {"input": {"val": "not_an_int"}}) + assert isinstance(res_val, types.CallToolResult) + assert res_val.content[0].text.startswith("Validation failed:") + # Check that isError is True (which translates to error response in MCP) + assert res_val.isError is True + + # 2. Test Guard failure handling + res_guard = await harness.app.mcp_server.call_tool("test_guard_error", {"input": {"val": 10}}) + assert isinstance(res_guard, types.CallToolResult) + assert "Access Denied" in res_guard.content[0].text + assert res_guard.isError is True + + # 3. Test Middleware success with ASGI-like call_next signature + res_mw = await harness.call_tool("test_mw_success", {"input": {"val": 10}}) + # The result should be text block containing mw_run: True + assert res_mw == "mw_run: True" + +def test_pipeline_gaps(): + asyncio.run(_test_pipeline_gaps()) From aa3b7a4893b2d3cf0841ba385d84ee77b33d0cb9 Mon Sep 17 00:00:00 2001 From: Gautam-7-7 <140533905+Gautam-7-7@users.noreply.github.com> Date: Wed, 8 Jul 2026 13:48:06 +0530 Subject: [PATCH 4/4] fix: check for explicit boolean error value to prevent handler return collisions --- nitrostack/core/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nitrostack/core/app.py b/nitrostack/core/app.py index e7b3b87..b3ef22a 100644 --- a/nitrostack/core/app.py +++ b/nitrostack/core/app.py @@ -468,7 +468,7 @@ async def background_execution(): elif isinstance(result_dump, dict): if "content" in result_dump and "isError" in result_dump: final_result = types.CallToolResult(**result_dump) - elif "error" in result_dump: + elif result_dump.get("error") is True: final_result = types.CallToolResult( content=[types.TextContent(type="text", text=str(result_dump.get("message") or result_dump.get("error")))], isError=True @@ -548,7 +548,7 @@ async def background_execution(): elif isinstance(result_dump, dict): if "content" in result_dump and "isError" in result_dump: return types.CallToolResult(**result_dump) - elif "error" in result_dump: + elif result_dump.get("error") is True: err_msg = result_dump.get("message") or result_dump.get("error") return types.CallToolResult( content=[types.TextContent(type="text", text=str(err_msg))],