From f28cd7001a63d3f4d89104337e5f9e6ef3b01e87 Mon Sep 17 00:00:00 2001 From: kennethreitz Date: Sun, 22 Mar 2026 17:08:16 +0000 Subject: [PATCH] deploy: 226bd63ed386529bfa22e1f2a856cd0b79b82b13 --- .doctrees/deployment.doctree | Bin 11520 -> 19071 bytes .doctrees/environment.pickle | Bin 178041 -> 177666 bytes .doctrees/quickstart.doctree | Bin 28010 -> 46486 bytes .doctrees/testing.doctree | Bin 27942 -> 27947 bytes .doctrees/tour.doctree | Bin 52373 -> 71114 bytes _sources/deployment.rst.txt | 83 ++++--- _sources/quickstart.rst.txt | 212 ++++++++++++----- _sources/testing.rst.txt | 2 +- _sources/tour.rst.txt | 441 +++++++++++++++++++++-------------- deployment.html | 83 ++++--- index.html | 7 +- quickstart.html | 208 ++++++++++++----- searchindex.js | 2 +- testing.html | 2 +- tour.html | 422 +++++++++++++++++++-------------- 15 files changed, 922 insertions(+), 540 deletions(-) diff --git a/.doctrees/deployment.doctree b/.doctrees/deployment.doctree index d2ffd264ee91b253a80f3d566d5e55c92ea81343..6675cb5b394d613b922c13fe27a0ac64bf5f2aca 100644 GIT binary patch literal 19071 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 literal 11520 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 diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle index 942eed0de0891eebed4d32244d40e15c0024a6b7..53efde2c1f5a6614077340257101b0b0cae2b65a 100644 GIT binary patch delta 7840 zcmZ8md0-RO*6+Dp(k(6BSPGVw0tJBzr7BOhmVzuHEL!miEos_@CLv8qlL#VET)~XmQr!zZj6;89;?~8Q0sx2OGwKD*^Ci#7Ox5wLEb#=_U?-bmiVZ7fBtZpmm_V`*x2 z*QUxEOI52|{)w!ybhWz0ncs3hmaO53_*W+J247*)K;eE#8sv$o=8iyJ3!$S} zTH1k231VH@VnxccY*8d##qY8tEM2P?<7~$i{Eod*!F3f0bmlck6dtcg5W7~23Zcnt#tCiT>|DEF9V!=5DU%x4yF1Q2n)c+&K&t0f= zRX|*fUVK>LUtMxG7$-%%`@s*2wsdWZ*tkrYwSXCE`d!e8=a&bjv#nm4j*#`XUfi)N z&OK^!q#tUN#kR%&ugVUN>m0J8MN$krB*CIo<4j4df~>h1aZZ-=1#N} zYqkga$O@eB0GHmqW4|0qeEzE|0cqzFrOc+?ekIYoeHr5JJ%QVVXO*` z@_RLF(f&sO{prBv;KXOOSh#9UptBc3k~rsJ3ZRxKZaCxsfgW(-!`n*u@bEBU=^=#@ zdn6f9OB8dC&X$E;$8u#M`uOv*;5zYwEFAe+LHeEo!n}krmvrI_r@PB$;Y<@3IJ)kv zg8qI^g~CMz9r&A!ieoPImc8pPt9a`h72f4PRcPq7Y?)hdT}G)(58>iVH=mH<#kUH% z@X4JBUY=}z?5<*-cyFxC`|7^h{pds4zF;hb3GJG$K~h~Dl(FeCWb+(&z`a;7owSrd zGg~?n3M09ZF{OfV6*XKMr&CjxM8c?MYEzWj(Hx^VN|-}#2M_pJoGPGB`o0Co^XJBd z#DG)#WVMqu1?~q64wt64QagRoS`{%|I*_V1rnJ)-yQHa&j`kX3O$PLAp3d$L8so8! zYGYbwjj>l3wQ*|Kfbq+hAY7{LrtaDP?&_ZP&el+$?WvCA>!mR+>#eT+Y_7(5`>AHn zaH)TvK)-keqn}Y57xdK_KkDBcHe6~PpfNV(sf~jNX^i6rtBp&BsEj&k<4}-qcU;(T z>H08@F~R^n0-X+*MvMewv$nygp?*DDiL-jF{CrZJl1^&#ygGbQp~k0bT(dDu`s78W zQxbYU8Auc+#Y|9jP$#7qtDQIEHcg1LlbgF3CS_G>jQw2d__L}sMxk15JnYsOFL|3=947Ucs(v=#o2H?zoUV>@ z;Z=?C{!Ddw18X$K!q?QE3topUF+%EL7yFJX5z>#!?_+7;d(Z|@O_c2Op-7gF{VO0n zE5IZf`@#?9vXr|3DuMpG8C>iu%tSYAfsT@}5E6lj3DTBD=q-zxOTa9Pv~DS!gM4ZF z2e1Qhd}bMh5qhHmgW%VjnLhdil!9_rKpHSTQ7Wy+?6P=#C6p-Q$kjn~*Ba=jpowb% zzp1!e!aA@j;=3P%T^5DeGw`n35OjTGBUAw$IS*X;#h5|g*aXAq$NoSOS2jUBFg;Of zA;LICoc1Y;HefmW>@lU#Nn6loOnt!T;1=xUg0+w=HAzqk%#tX%KW7=aczG*SE28T^ z;N@bn^w)OqDCn#mK{4zra4G0pUkA~cT`)sI7wra{A|~&FD>2l24l-zmLol9RJqKBI z`5`z-Lk~fFD*S}lj6=|gMjl3N>KW)vBaa|<=L|ef-#voZte+r-zH=0@#-E@qjXs9h z`NNP&9~?uh^aymNQ~rzC&GV2-ryNJjdI8eu&Etq!k7C}-ClEV(6!ThsM(pA-j8}LP zv2p*!c;B2vY}9d#xA_!e`;KG0tX~j2a027CKaJRspE2Iz(})c^35oQBGl*?C39V_y zuZY#3Lagms#NvNJY~fkN!cJpk{8SuIUpb8xv^tNN1lvCl4HhpfMYPZ+YHKmG7BEE>YjD~5mY1A~F^(}H0#KPng^ z_&EUwc1SRU@e_g}nja4gp>}o(GQ`@SWkV}E>ksG;`Sd@3Kw%A|&$own@T4FjxWkoX0MEzRZ$l0{Sj3^r6L-OY!>f0B9S81mm-hE~OKkUejhpWA;_lpo zfyjLNK8(R(E)MdvJJ1%7pd7!l$PgK@ zQqKbzBTuhmrKQ|vrn4X5y?zxhdfC-&al36UC&QllmSQRBcK864OnQ|$M8eZ=nOw`EbCKr*1HZZqkC*JGq#1bZm$(`hP@ z4zao2LqfXPIWmh51JWr$jo^gr|DdygJl?YIz~&JCXIBwkqnm+rR|k2MHUjCXM&{72 zgygD`TIvfU8NwrHq+T8Df9Ntox~Pfs8IqAy>GI-_PHL(^YeGpzOPAARd71qnrM4}g z147A@>M)BKyPFzW%8<vL z(x1agCpB>^)kly%YGgZ`=@V+?OL^`eMv!)DcxMn!p`-6Wgl4AS(0m=qqD${Ulm_3+ z@LuXL-vz@YrC9OjNK3W0^ZQkHo7-e}yPTRX9hB|Skt9_e_Au=kNqVS}V~m(Z=SPxA zHGD$$|16TEtKn14X@wnYwEaF4OqrC#ep=No+d0|lj3RB+0WZq%nkdp*4PTPsU!q8g zUiAbucB2rjUBO1Sf`{mUXwq5DyxNUUi6*_&&IRgK^7MlVH^Y&CS3Q@gOA zLZa0810}@N7}8daH_3eVdef{cp;gf&gkDN+F|)0`h!(_=j!;CcapW=F zQ{Ijvlj5>^goNPMZ=cFur(Nm2IFgal8!>B+{con8@uU; zd8`3>hW8D1bNs%z{4v9`1OFMQs`A?#C=`+Q$jm4rX*lG`T1gS<(1!bcZn%Jc(cG`h zYN<3cpCv^k6UjS@NLw6E$Xb)EwJql0)5X}N23k~1TDN0?j1^$40}K_zLRD0otE^KT zEY^HvJk1#&gHa4Jl#LY`%y}~yzYLnZ)eaMG+06MDIobVU+|U|m+C)=Lxz*z0_vbO^V6F4}@W2rV(Tlf}(20GJ3+F7|>+YAmx zF_#Jqf~(UEDEl4E;}_FgDJb?b59x02y$Lmpc7W4)H@R|eb4JOTeR zD@ghP9)7gJgp5QD!{o5xBh2q7gV`_%?eWca8_oy!pawdy9>0(psJWix_T`~oH_T(y z=j7$f4z_|D=NkmIF}RmC&>!pZ?a)B~tS9Yoh*`-Wima97u~hE(kzpM=#ss2b%UJS> zD>?gBnN3%c^gf)u*YF*(;{t5{r5%{c4~7G1(+6zI-qb*^U@pw8Uq#xrBc|4>#c$;@D!#LQTB z!0cv)&{?ZVW*SdX2gU|Q!4@c#Nj3-P>{w0Gv6|DXc~K8n^V=wW4S%YKui-ZP8j^~X zw`6d`8j^!&uQO{%%R+uX2iwZO{*?zZGLT5o|DITWNCKp6{0FQ;OcqLZrVx9cRQeI= z8&|`-{i5NYSbGFExfk2DI5xphq+om}uj2Aea!41Vu2AfSf}R6hv}OhRKj*n9Rh?1Xx$(SU%Kj z7I1q=aT+24MU-n6`eV^WQH1pXG+ZJe7~~3s8-Z|quc~?`-F)*$zxRIctXEatT|M)! z1mAB8-a=xh=#18qBAe0SstPJ8H5QfFjOBlERq0Ij(lV=gma8hv;WRo+Oa`OFVJ>&N zszRN`NKUt%j{@pVqK!&zc0VmXVAwjwiD5?p95v)cbsYPO-F&SA6_PPH@BsYcUu ztG!72W?)w5bYCAIiykjk!3vyGTIl*8*DEf|aH;p8Xc#Ju8uT0tljaQS2pQ7yL0tz7 zX>n0vdR<=fszQ`{Qpz1p5iV1@HYjZZ&o_>l4QH1L>@t*HCbG+5cA3mB!`LO8T{74u zk6n1;0><$?CdOsDoi7gXox{Rb>c;L0K3S6$z(ZH+j-C+`3ty>o?hoNQmZDO(B`lOn zEKQ|uRxcs3RF%4)rwECqtJImQg~U=;>K=V8q)aXy`AJA4g1ES#SJ+7@En~EfRkPTY zDBaJv>Q@|8WtARfn54#xaOr&JdoWxo8~RTe>h>MBu06aYZOGmO>5?_4UXpXWNdZ4c zNi%aZq|JHVq!YPEq*y~Yx@!@1kS^zCVEf|?@ibx?w3iC=Go(7yeO3z{r87*(?B?lUxqKELaa9pZhiWgPGGCdTnT?_wP9^M1zhJ|AHm@AL1B<9)uyxJ;?Jpev-i zqm2n5t*VctbC*FI_tdHJOls|}HZ2!Srg^(y0*fwkMq0fg)E!=OT%h+XR|FbZ6yXji zJu1*9+X;creJjF!xom%naJc+?A>{84b${VJDeO1SP+}u0)w~(tmS=s}Vpj9!L18xc ztzQH){cXWWEdI2)ifxlEf_ACDw>01Xz$+T7O`-u z!|AC&qN4>t#9ylTI@0~k_8lVDrma!*>I00mK~|zOWNcMiFALs zOXWE}m;_4M^CR8MzW5i9=g!})%+AEGnglZO>lM7nZQn{F<>K#S-Fx?a-6H(^zd~>? z{=T6Ft@}}Bvi#s3}1$U3XoT|iF+Nieu?5fAMu}O{l-bhjGhuu_gY7YZ{_mtax6vc`= z69u>J&j3-au;_B%W0C)0f@1qsvnI%~M~X@H1n`qOKe+)uOT)p$hs>bu@4!Rh$wKPn zRRmNH@>yRPE7Z(b9_FvM_LpDNX{=WU!Z?-nlOT<=F~s96V=ARdJna95do-emA0t5N z30Ft9_Ne8Uwi?IcC}?h}-d}#Ry~goB9XyV_U-D1U8g*1>tva!*Ms1DJs^`Q(U)AXL z#%tBb6Ey0sJ+$g)6E*6Yy}ar#r+~j)nWX9D!DLM*Q&Y6oBl>EBRXwX!*Q9C&(vYTA z-+fNy?JuVf@HUZ`^7259x_XdS{c*Y~xW9aLuvYyfL!*9as8;>jOB(gc;cB%`-aHbN zU4k?3FW(%cRR`#yw|5Hs z)H15oj+SE0x;RkcF{C_B!*o@5{p6mdTJ?)|O`5mMv}&nbqdwx$sxMcln(rs~o~ha0 zi)U%A>)zA^JO5X$x_P#y(jjxS>WT9--qrJAM+m*O1?=pdtbjh=rri7GA&a3SFuApC zSpvBVd;EPbJ3@kK3i=-x6e?_5HIxEm&>^*8WAAka>3VUbjBi%m0lC()yc}`_m#`8F zI7h2jK?7vSZ?1;j=qeAa0Y5^2-;OomyP%P7+5ri2zYn1+Fg8*)u7x6n{iP0!3ael5 zMZf+C(iJpv12idY#6~Dl*vd~~x?rU_Z~4n1o7hNr?D?DF4WNT-@o_lJ5J&&E6-LqV zYdt-=wiQ|dlUvIXZg@>$XZ;H-Kx--b>@!5sX*zLOQo z(aZk^hr-%EhZzcc=L?vru(NkVg~IxN1!W4mV2>9K{sv|%=(4@63?8@5KDZu2=O2a` znotkfwD~Y}r|atB6kTu(qG;`5#BLpfPBiuiVvWb48|`)!vEpB#7yalcV!!vv1TNe;|f0=g~CaJYv6}z#88^k66JoxC@!2a}td^3nS^3OK@~JJ2dHE;Kvm` z;b#^7bbe6L2k=u05_Uw<`|Kbu5XVBq|FmVnuUt;l3 z;D<5pyu_l@NI@DYL?eZ1q+S{+StF&WrHZPsBCCC>!D*K}T!94urF8XGh>Nx-`S?h# zUh`bDU5OS6k5uC<4ur;8ob>orNb5O+3p4q(GX@-InT7m&RY3+*af!9C+-!5@xh%73 zY7^ca(&>~Y+yXW9P!mi+^2{|Djl_8kCLsCt8V6&q!*G0(sG;L-L_f}FntFC+NeT}~F><+{U2 z!g${JQgdO6F|ijta~GFr*=-1-`adBSJ*A*)2(gOAq;7SdcD!_*bVwAwg_+Y4^C6A$h(9#^) zhyQ41C`p*W`(Wl93Rez0{3Br}eAJ4JQHS7 z;dM&~lVr8)M@wgrzG~o;rQ1lF8u*NU=}Y3I$F@LCq%HI;A#rVr%Iy_pE_DhwSJ2r& zbm}lW=n^1FYGfzf2c*9m`JCCrwJXO}SZ+2sHDPyi9W5XvSnaZhPVgsfX(b`;)bL*V zDIvYo$bMEbO&!~}O^sX=Ip+-*7L_d-o zrJ8~hY-%>*9vt_hes0g4FO$4oBfWvN-HEO`tm>&=)$q*Rd}p znt|P;I|E2>b);rtjsxR26OHv_p-a67G2!Z3A(`5Aq)(WtrGCkDZUE^)59x?bjq8%> zIUPw>L&3?kOCU*7Lt)8uQXolGLlMceGEh5h5jbO(yL{T_1d)#IOm>@-ed<-0)+U*5 z3L=T9vrjLr=ZR}!n10tm}Gh-NITwmk%!`k8qLIXr{-Xi zqHb^^tqCTnY9xuC4kpj3krWymqD7viDWN1avR~i6njxeKIQ1#`c>$+A1rKEKfYGXH zO2_V11x~B5nVcoKZr*dc=D8{?dPHZtEE8FOCozkVEPG*v)jZth#E<$CK3kN#-sVu! z8E=F?;N2Fy zi|qA~b;28+zZ5K1m zyYfiqc5He5zuu+Ot9hgYf8bU{Ej>uukKtx~7)9mAvf?+aF1|2DOO&MChWAM60S9=` zTl$(#t@)%2{}biu>1gp+rjP@g1<+!mG=c-#_Hu}4ZRsb++OSy=7+sl5LfW=CSCm@~ z)9mFqD7mMBbjl27)2ENo$KpzhSwlL67MYzT=xQ*q-j}T*y|IlStRV@Z+%Qq!3uif+ z#;qZ3gDT1jl{su&Lo(1Vxt7EudAXKEBeB$ySR{Y1)OK%QoWeif!^^@`X^Dc(mASAL+cMP;~0S!#9`+Y4E>ThJe4ANY`T5Me&k zZ&8Qw-miI>ZTj8nFlEMaV=3RXv1@tVnQQsF%3I3^GixpBGJyMC(l=^@Dh~**ysE#6 z%1}>j>;;%r_TyU811q~NNT)j9kwJC*c931idoZ((q@u~jI-dLop=+$;HR#rnZm3II z$1O*#<0d6STDXq)e8W1Dn9fSb@*!FN1dE7##q21v+Z^T;g8}~zwelHd20D@z#SB8c z2KMQ!$nf=i&pf}L^y|PEjJN+rlgV7h?r7{*X^X|{{dYH Bg&F_= diff --git a/.doctrees/quickstart.doctree b/.doctrees/quickstart.doctree index 1c7776666bcfcbb5dae50f156e721fe42e1c85f8..1e297e38278c67172d9eeb7ffd5a54ea400340ba 100644 GIT binary patch literal 46486 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 BUYY;^ literal 28010 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 E3xoA74gdfE diff --git a/.doctrees/testing.doctree b/.doctrees/testing.doctree index 1c91d71a61e1a61f0c24de59ff8ce13d74c4dfa4..e8ca6e205dc384e8dd1481aa7a6a5d25f3ffc0ae 100644 GIT binary patch delta 182 zcmZ2>i*fZWMwSNFsR|oetm7E7CLfFwog5jL%9sgdg~(3HV2_=mks;c{5?Y+9Fr{@$ zR-C~(6C}yyEM|>j_69X%dtvz`|JO|^S$@xhslP!}3 zCi`itvir(10`>ja%%5Dq%$TsbA&rNH&6t&eARJEe6>3fr42&lc#58NUMXGDnO#NhchoRH?^d)Ahj6emMs0rQrV8Y YAo@Fc$LM4H!kibGxB&jS_sm*@h z`JeaBOusA(+rb5`>D%{zo&UVg`Op8vhIhU2vdb^yf60z=tJLdO>v5skDo62hvY8JW zQM0@J&gDICT0Xx#k!SJ-oL5WOR-LW`2F^vYNogc8KA+%ayB= zEk!%O$`#2*J*{KO`g*k)Eg$R6g-I^#c01L%UKh*Z^slIvV@x&KRH}z@jEDGpLo;ls zzc!b9ZTDp^ES>2@^L(e5Z0d#!{AszeqjG&^YvqNND=If62d68oMl{{&HJedqda>0x zGu?^ecB@&&SN9jD8BeR}O?R=>LMO%oZoF?@Zw?FIf)#}eov>Z;U~?*mO*vVq#(@TD zuo%|Q#6h>xY4sK=!3i_W?l_oNP>h3WGbl&xZlw?$2}>2zG8dg|*TZU)jiaE{YL@WM zWDr+ci*dj?SDOoXT#hD#uvrdLZvzdg3(cTZX;n*6oSTa(RlHK|7J_@`gQZq)H#V;s zEe73Iuo%r@^s`aD)h5bJVl>Pi{|keKR;wIf4yA}M)}ye~%whDPfk8V>X*u5ka5)(@ zrJzR;%7iOuTu{id>(q`Rq4LEie)3xi7mRZ3%qbFgBzvWKkX&^%>Q=CacUJ4&r~`b_`?1Oo zZaUvJ{B%fDuT!tfw6`Hd6I+JC$=A-_aLED?7M!OQm)z~BLWQ(1;0pljU7O$sA{GR%6 z=0kwqQ9x{0GIt4JvBqMfq3~_H*=XT>TjzS%vu?3o1wS+*#^E1WBY!DYyHUgYIeEeC z>|IAr&dvs3d}0|p2L=z62+CnMRPeNzQ#og3&!##$0oM54Y|`DBO}GM`dK>V@;Wyws zR`mwifU8%TlLOcr{4R$B*j4EI>>z6HYHH@ok8uplrDw_KrrFu!56l>Nl{%0XQ4m9J zfkA`wgMDYg_Fe_H|6se=Y`QS8TF)LmVEt!*W&N?0!1`GM)(Zq!t4_$l>}UO+hlANp zjPRaeVaAtN!tAA3@IwY{`v<(w{{g;2!MBK z3{B75>zi--eGdoQYb)2+w#gX%=VI6)-Z$V~dw^u45<<(#>wLE4jIti9!B}($? z+m@(_guOa6+|a4v0n~CJc_ZwWDo|3Oiv(JxQ>9RfF!h&UzSRlhYNK6`a#6F~ZdIX+ zOa`z}f)ht(P6mgM-viZTsa>TyvxNTILAg3VAJOJ9J&NjL9_CtY2sRpBaUpmphK3CV zW_DH&4iutwfp65NThwzPE=y2`)PxT@-RAwzij{dtbTETN9)fT60AMhDrH)m*Ez?7P zw1VKOaZEw+7Y%@GpLwp0i@s+5JRq+@zsr*CT7N0d)mx=Az83Ruw}$O%L9lNkKTTCB z&QBUlIdnP?r6&LAo*ZyCu*y_C(TN_LBr>%39LxpykAD^#QMnohhl0oRs+)ts=||7! zasvjSCXj!05;kH~I#Z0!L1Tkjap+Dc<A0Nuj+v#$gx-E=>UJqBJ#n|A%D!;hJzI!AjfFq^0 zY+J~*_NMfgZ{FE!ii>t?P9}1e6QPqMSfF9(sV6~3JrVu03QwsJ2`McHB3S^{w!yON zQ1Q!wxJNWGfrJ)dVnXi#jkch0#1IRhO7I)&Y7xCCw}_YEm=y5%vw{aEqEC}%en?IL zX3-4Vt#+>-cB*f{cymj+SqK{VG=tYwy4`ksaC&-PzFU7{q19QK{?6H=qvKCM*FfO0 zjQ88US6)E6m*K;v`Ur0CTrA1Nuy8X-x>3Q|8|%ZFvDRi!$$D{}C#H{NBj(iWRF{(* z`t6#Cqz`eF>uX#X5c#rXV>j#oul%*VXu#&HB^Y;TvQc5kvLkf&M3c9jVC`cZ7n1RJ z@oxf=r9oLpeSu$I+av6CL@rbzZd$HvQ5h;3zIfTSqW!gjmU zYImx%uodAMu@@*(0%`C)Ql=JjTIYvEJAScy_JHtt?wdqOM)61QPXVEJ8l4y?)Twvi zOG+1g94srMvUxtg2fkf!*WfPw1Pc=;qqJM9pRHyQXfqK<^?7Q(TK%02_D+)XNjF*- zw)$`vfhcID^LaPWBm*Y(@=f`isHVDO=p>`{>@@98REnx+BdW7z8c?}81>HBM+Jn(H z?8Ni={OwSMz#FmgM;<5tb!qsGussvQSC6$|3cygP zc0;JLBGeFqDT{GX>mgnXc}f3js0<0`M(9)*D#mphNOngdXFRgX5liPl*bMHdh0TQ) z@+oF#X`X?f=_C9203eEINn5^%QSO5rE#eyFKG|nCKofC?4b%m zdTNyhOV;zN*LES(b}->`G+|h`8W4;PIfqBtB@Bm0cck#hwMlk*X6BR_zn&f7{}`Ab z`&j%mt@~K~SKeQpha(KAbwpfW2GT2@yVd@{NI<8x|90C#to?garo(TXh>$4(8=;PP zg2V_A?p0DYj=F(Z?n+UH-;FLcm`WXj^fN~9_b@AkQ8Cpcowp^n zClk$}@NqNu0#U?fYJ|;jA?oZkJ{wp~;^Gm+0@49ja&e{C6)d6hNB3_fte=H*2@zuU zCC*<7c6cGG7xo$41Kn?CKv!|V-??IrW)H5+Oy2A7W10R2Zv|%{TXE;b0f9G}?Ba&o z&*Q26OxxA3fSyT*JW~U?ef8>GfByY_lk4BwS z-v;2g0JQe$pMhY;vQKAHi0#>@zYdw(fx5;G*2p$HD;NXC4hg$(dUuti>$Qt0rP zqu;P>N)lcT|C$exB+Ud@@F|5`i>>Bv-JN>VpkpVjBF%U*SM5TvthW{xAblC^_u3*} z3KJQD({Ahp-__oX^z=O@@HZKbD9tdw}BLU&%G7p3Bxk>;*H0zAmjj%d*bfG+>?$_$7MI&Ad-=k@ep zrl-O3nq!N+o*V`G-jIDJe^qvq%$#ZQV9H)(%{ULkuKHZ)&Xm=%4@ow4x7zh zSTD58^G*&wJ)K)g`a=NjSdw%p1rSe?-urqam6hOVL!xx2Q-!dkel4OEN*Y;1P*r{f z%SoWRb+uEg8dG@}Ke5x=?t7|Tic+gO^)TV5MnWv3&rt_A3_gnqQ3?};OKUWczx&}p zyVB~m;%T@B3$^wF(ndAJiiFsqytJRLwW9@x8kcA^iz`1rF0Q;W1u_p;9={Vx6%vRR zw7{DTdhpq6j*RT+C~Ops?4sWxJlm?4VY)%}hxjI5G2yTi13c=8(=Jn<~N=dC_h!_Tf3B2O*E>-Tpu{SKBToQcSi2QD%+ zyjf*eI~;#}OKOYz@keO5P+~M$s4uJ2g%7qoJxxdxDA1_wt7Uw@VF3!I# z1rP_sWINpyKwqXgLi==?Pv!@u`}NmwsZ#dQaChB~C>*?qM>-u4*xfKXa149FVC5RW zI*s=qMnX(#ykD~|MB`0T8t*6XL8f2_x}YRGs>I&1IB6tD7^%Y@(mj%cLE>?QfMU3) z!+SLIOA$|hr?g!p5UA8gleZj{)PhrK#x_eKI>%$yNK6*-0}l|6sA5!+QaH2NLnfmb zVK|80?O>KT29LAX0y`2gj&nqYVZc!MgVmQKGUnN|Adlw%pWVJu62yMk+xV;`{85pF z7Vt&AF%u7m^H9d;uf?{CZg&IL z+9F+_nj9FphgTxFcjkd($h1^6piIh;Ag8w$o1mN_C})6ZBeLA)(dj~j@(b{IyaF#N z5^BR%bAa}T|K@|k`HOIeOAQuceFW|%mRn_aAv7yXQq zcdxbYpEpSr*hh7A-ehbeg&P4BDBQLPauzx1n!|_n#c+u(F!EkvQ4`1V>1#kj8s0|k$P`@^;T+r( z3W*F3YPUSt@N;^N4l@ppgucc|`Hv~gaM+aWQi5_ypgJXdmzsgoLi7t7?A{A~{B0EX zeYBBlZHOpLe32d?n`E`#FYS1AbeSj6it6RcXu)X z3BkoihkFh_Uz`^OrmL1@sYtQZz%ox!as_o*pzw&sf+`u}FBVEc=D)15Bk zt1(_SH(27-!_a@Bp-Mt`{^f-Oc_Y;>(P&l+X#rkid4zs01u{2t8o zWtY>^9zWTq6m`}>;mr6{9*ju!a+L`I(Z!B z<#|}jrGn-o>U51gE8@`!vS)7Dw_l7T`}0aeDc@@n(K!ZAz*uwXSvhyh%RU4=9E+EI zIJKP~FZ-{kwMGUpJDb`O7ajv^X;foI#S|jzI#Ows6PyyA&(p~> zzl&5H5CW&x+ean8<}gp%tgex-`kPz(-;A`uRLH+%TZlsbk(5II>64%UD9I=iz*2>D z0j8m$R$BXTIrnB%4TRQiH(XJ{>e^c5}bM(Qh^kWfK#6#C3> z^3`}lv>HXB@KZB@V}-|@rr_N~Nk$UA)3`tL3|Scbk{4-d9+m%--O^E%>W_NJkX5Sx zRD`5#rqQNRbA*~SwgOKS&s-rJRXP-og;Er%GjHbu`GTFS7ki!h`C#fh`2=YYo>1g5 zRh2cB9nxi9QH6)8h+SPVtaRsT@XRQQ%2T&3Dg9kd7l$>i7XMsbo)xw0HjF81e>a5` z9#_0Zc4kecnErVfo=C>ihbQhta>@)U`oPql>9rZ8GQw_^UmQPtAr|iI&;11+`Qdf8 zh48~aO!33dO5FyqCKkdJ#{eLp{jXp%-Bzhp7ndD12d!L$@fk2AK45GK3PRY7VNM}r zavNA5i$w&fgd_2K(D0g~Cj^I)t7QaHB{~B=gdU3?4pxb(c@Vlq*hLa{8zaO*c9@W)%e#b27;^!x6FkE7>fsy{G5Ip87$G6kIqjaKt8>_}$L{SYjPZ{>tVm(Q zZ_iq!j9q1gs99L7!pLQfD5^%7Xx7H*THxd#)TOBJ%5FJ zhuSQUn}ZJ=vK6nRKTH)B)#I|)PmIG?{es@Ge=vi;A_z&HCdOzqCu{Fn_$0JcSR%I1I4I{R4O#QJ8;`SHu zCI<6`^z7S)3z{nYt}5`WW@jzFz>_d78BkJ-L{JEimi+$$N=Y^4$$vn8Xp#|jCuH*_#+UtqWeM`aH_Nq>p8zggDae>SH2Dc^siUI*aicK&v~JGHpg z%02n%q$j*8=jQA^z_*(*p{xdTe!S`3i z2*V5Jmf*U`poDowoM4XBT(ub@I?3F)6@`djsWHp6@tKNwwn{&O8h(9Z6jl*lq1zbL z$+?Ff?)=}8cj=!5W-D=r&)2{mI3*3M$V_T`>bWv(jUoPF1a#s14qetBqxNd(fVJ1)gOzJc`DC&mV|1p>iwjntHWfd40=yA+&LDgy zbvb8et=c3C6)+MamZqliw%J2>EGd9`1$qZ0yJ(U4tJoGA*&HEm-(&cNVp|26W@nv) z`lf+X@_=8+?5ihdu3kBFgS$J@OY-`AWv0Kuc)}To1e_QaF5Yl`roR!>2l&jC*P%kC zV^MdHKh0}a*$}ViJ~Kl3eAzcMr7Q5wYx&*d&uN38g4b7{S)f!3-60M#>-p7dTOWTO zsag%$Ijl`{q%@=ReJMq0u1&Jj;@niMol7p~#{C$$AN%Cn&|}ock#(JTBRf+w^rs(D zm3}8?W{y}RW9=6lj{V3;aHLx+u`R@{^|wB!Ikf>S2~%L4I@BlBSdNTUM3tuqet%2( zDsOCW3u$1;6Wa@>qFq%ef8zP3gfn|yuXZnHy?2Db^qn&NO}WTaRd@G22j>OOnNWy1 z_XX38U9b)Qka;LvlJq8yuX$8!frAT9I27DNeZ#r1gH&QfVp+9MoMqI@aby86k+&Vb z2NTzxJM{x}cE{+ZM+Aq53zCW8|Jrj;7Z0^55 zO`9Rv)8C5ia2TJ3s-xb1?uoB>nD1kJ@?#G*Sy#`)>dAmxt}fQ%;V5m<(I1B+ogj?V z$x294<)e$rxj8u1fXj0oToV>`6nd3gkvb~{FEPBg;(3#dce-%pa-}Ky>EN`T>!3uh zpTYjlOEEQd=4$B4dFc7kk}UfQFlpv zHnx7gB4=}up1(0>SfvWkLE*A!V*ftvVd8K6PwGCPnm=`Qq@~a2wbL|syn5bcTRh|E zsi*~z{zL51SU#O^q_)ZP>70H*>6JRBLEKeGMVK9on-7I}G;bHx^xp7nIDVn@jTXXI=>v`pmI8DBK4jJwAl1o-R?-rYB+B8&D8FsiKxbnU^t04PQn+;VhG+kaN|WJk~hES zwK?@tq17~KekhobU-txu5RDabVt1{2R6u-J{Z=4xpz-> zjdadND7FCe_myCa12t7s;$^?n z(XC_(hGbHd^5ZAO*yL5c)n=*QV~E~pMX1FuglpUjD5x6d6sF<0US|LBWU4L2bq$FX zf*Dnz6cuBT8DM)eh@TSy1%!uzi^!=;!-9mX*FDv1->CERL{23Gt2E8*9eyQ-cs2__ zuu^Z3c4m;ZwJ$?Gt`|i<_67~X?F<}!GU9r&WaQnPv0C%*%#hP&^O8qX{<9US0#9(v z6-1?G0-}2DQ`lq=h}oSQ4y&(BV|7Y75Z95efoay9(^pJ9NUlF&o)#XE5E;uAY3oaL z2B@m`jTuq4CK!r8lzoU?LtZ7QP9YycEjBUbI{7`w6r>5qTtwJ|Osv7l9w;OF9*Aml zz6CLXE=lSe z5pe1#Uyu|{h+<%`Ts&|6rG?yadMm?*6zsAZ2s5{MRiFwi6UWdKGq0%&Nwr_Y*`2#` zi-m}KKk6+I8u{PVKqn$=CM-`*@dOBQI-wJPl>?G5|W4{s} zBW#*o%v}_!Xy}dgv7r}}mLcp#$&j0^3br=p_@YDsBb|iy(&6xLRfz(`Tp>}^vlSBM z)7*y@62mJBDjZd{9fB=0aON=z+7w1A}LG$3PysMIALBrq8sKx!d) zEo?e*u3JI0b)f<_L}3GNBE_Lk!~O;F#@+m*Hk_LMU3JYD94ViCI;Q3k!QW9onB@I#I&x1xA>y0Jbl}4q%w9HFSd$VJq5ZO`%PC8$G$@Jz62Q z?&NSQ#Fm-_458n8_9B6!AFgI}=fWN`3PVUK=fVLex(wFFDI7=0p4S$L++raLcMfj< z$W1*A{NBtu`ZG@BYc8bMkuoO}T;sm-LIE-Gy47wSr52C#01!^CMEOa1j7TjjGaSnA zFYMEgG{PN1p@w}Z)u7%3<;F$HiaL~^<`+CSHru5h)fz^10B|hBXkQwJ&M-=@l;T1Q zFj=Gqn@z7?@!X-~8^+qkS7oNqCxiOMKFif^*DkF#Wi%C*8g-L0h<*FnV$ONGz}h(-J5fAyPt#bH3ZrZN`w9#>rB+6 z(%?xwClI|$PCjstXPs(!7(9gHQND=cp&zWD_&z(kA`5l@$X$py;uYT$!#4Fm`)e7{ zdYgp181y8Kz}q0-z1IIN)4D(WxG@}W*o`^JjrkR6(7$7|G19Mn53Af(ERc~Mua5Im&I%Zf=+lt#*Q()DE!l8)?S zKmf#ckvcTs{xxT4j1- zuroQiy+Z4GT!P=9EV9~^U!gqWLc!U09FLkQOZ_I=oqJ{ySOC!rX>?2#=E8nkWnu=6jkG_OXtFFd!oJS4D|NQ2rq8 zTm)nGdF>wPbyqa3mgP-pwt6QyRk=ZQZA4Mp2=60=qQZ{xq0@Q#w)2lFt(=h?{FkEr zLo#EsUBx98Ir1sy+DhMsa5Nu0O%S&B&oPkUSoY5yY1;7YpEp}!?qd(*LYfgNF{TBuJTzzZpqVH~ zE;R+wK17t`NN0zau`r)YuES>Ll9aHce^_o)k;f67zsS8SJ6G-onTy8=z9++WJjpng z{3&Pp8!Q_+1M#$;xs(8Lie?r9!?D7L*4*v~_U?WCVO+9G3r8Kh)mkR~f07PCcYLQ+ zN1^J*9F#2W0t=)SeEHmK+jvc#YsDBwu3VY<#oO#BM%f&rbA6xLpVtcRjkgMK670{{ z#fF3TsWiM(q9^&sRJyjXcU=kyRf5Tl;}z^UpU)qLmlQfQs!;Qdg2<1lsVVsv+Oa#! zubj405dL6r7xiZ3w6Kl5s-%UY-qADQJmDNG_cF9MV)A2EJc)7gv*k( zzrs3XeR&Br!061!5lGWwb&7rd8oioryk?a#J)-%A4AJyutA=tvU?}%x$^ewPtMXoJ z&)yr9?MJ87g*0T-lzK)qL=Rf#w%QqS!cA(SrWH128&A-~iqjWz`*(o9)w>hwA5VLP z?xbX5VFp$Ftt<7Cj%hLT_r}G{Y8rkXW`6K6_zpAzcwmf&0>(d+GmseGsGfr?QQ|}8 zWFVSW2u$d8`b!4RImwh2bTUp_yl4V%PRJ&&{8IJp3|O5aY&#I3hC1Q z0l&EV!p!s8*+>=SgICX~8}pZD#vaaoI)k&GKEspg-ogy!<=jKBpFhj=)0f~R%Y-9~ z9<$8{nb&2XslnNZD?|ucGph~q-D~}o@h7xy()T#@nZ{OcSItB=>3#O5@~`7hWrNf~ z^!n;E8)O7IoHEndY#w=i?8v40)ChZ(ik4^RGMr}8O4F-rlsucgIq0~E`jPkf%I)B=yXKNZL8O|Mf1}So$kATB*-yrUa&1BY~D+U%|CotLOeLB z+tzjFafi+VPa(?Dtli*Wa^np=rLE3F*hD=NQ%FGK(h#{zxTR87fM!^Mtz;ip=zk7_ zhfdrtm#kLLp$=}Rg-jW9ovQUei?j>n>@SA*OLH^9K!@ibwO!2dSNhKzGV>WWa}WE! z%kIdapu6wNStQn)%|tN$G`u&)rF-gC_;PkX#Hq?Y#Xi7ma(2Izz&yLZfQ{)!cp`T& z;h_}Wq^2zoR;b!(MfOq1_G};1kF#YFjvRP=SZR|zLIsx&U4L+h$c1lwQ;Ypn-`a7a z`#PV?ojs6U!vVF10|VAjJbSfPme{& zKbT&sCw{gGolYq}>Pa2-oNSpAqOY(K_TER>TywP5LpTkPc>rMQ1cy=f(@5%7e{-wt zjo6UnY-?xhI*L)`f`cYCvm_9hvdP|Y#oOj z4#PnAs?H&&3p5UgjT{a`QnZvuZ&dLY6RFf?%RnJfz*$)O&N}L<%IPgTm7JWY$7y_Z zI=Ru3YouksSnfLwP;5cUVERQcfm*+ByO8dmovZPL)SKXyTz28 z7oh7%gX};qC(nx5YH)vfG*K+ldr~a2hO_RZV5Z6i zQ7}|N69UfykA!z{dW=j25&0-sK%EMkz--9?_Z+d3tPrylOVQM<{okrY4*F#{-!{rE z!gz^wBd{i#$KVZ$6O_X?&QhcU<<4^5<}buUOb>&t z6^<#EpQkH$vwh&qN{*){`bv5+wSv`!)Yt?t~MV&&U^#iN;DhIo3N;wFPj79H*xtJ7bbN1g#F2x~mb`(>!Fb$~xw#xD%CWktl(L z+!X&tv~KH6gvT-twQ4c%aH^>((^W-2Pz`I2%lo7cUN{=#7172dGi1Jn!DqV!!7E#2* zxhtkeZ0Jd!E4HTB&|mtP#E)M}uOa0vNPggENL*a!YGy9V5_du~1(g02%7@hsg$xgI z9!WTA0Ve)L-u+73{AM{i&TB~d3e;m|f9b>vxslB@KdnoAJc?fPsFGDL+map6q$yc- zYbQWImb&%JY3O?x^qD(h*dSL?UzJURWrd>+xlZ7{r5^ALrC-&(4M@cnRt+~kxl^lI zUxJPXWo?1XSJlIF<&spYKEqu^Q0}bW9fk%V81OU~E`%)E8JwD#fj!gZ1>6W2P*Y<) zSd1QF0I+%d0X2*jm^dgV2H_#K%_z8LOzaG7|kI7 z!DjQw>tlDOkN%{?v_Fn2irXVk6KuWN^;scy*=&JX-$wPoYyX<@SN9TcW3fGJY^^s| zcCJpn>!%a`TAB>fOz0E9D^y;C&rsp!go>q$ZlTXxNFAj^`EahZ|G>-jvqpHXh*Yyf zFMs8}eUpsN^S_&Mm2n${BKE?5+!WL6NN);eQr6bKi%Lk=0n%_aKmKEY|m%={tnT)rg6R`$<bZl3l*^1C}U1mFEpfy#UO!FZ9XN5Gmfrt+;K5 zB@Rl#o!~;&x0rtDbc#=owC~g@K5kpc;y<6#DU$ac#Vw?~`pOT8F&iDX0iwXDOe{qC zN1PL*GH^SLy12W9iB=*!oAeaf9Jwf-R2xvE@$Woxn|+N#^(jJJr=N*DheaZzFf^cS z(iTc3ASj?I-7^HA_*T!=qrd-Up{gXEXGX&NZZvTWm@0Fkk*iN8*-)MNZ_Jzft{q*ydrjrcm1(%V)Ce{pO;+gxHs6QWu|IAy8Q zA(gY3$D|KBD>zIG`4mbq(j%EiiSGp4G{O^wnmmiTB^P~q!J5tY@Q;_kNRe+&fGkw4@+~jvT@OV%&G_})!&i>~0dc*h&S~uxmNY)3oI2L23V0O>U zZAbbCZ(@b~Yn2#?*1 z-|wkS#sk%z8Mu2if^=Yi&Snph|6vy5^>ZxK&tOX7$U-RxMu&zsaCQO1?e*WJ_d4w= zdl6WAfp=9K^U5-Bbp@t@#K89~L!QT4$70BTm!79vT(V6u$SF}TJXK6TN1{K~CL5hv zEmKx>o<17b`r98kF=H01zBn3t<47A$jqCet3(>f~meRORRV1W~0mq8jp&f&{6Sp(r zbt?BjrwW6ZZXH!ye+Id+u&$)`l$cV=+24qG7jHFgfEW?%)yK|a-d9?x$VIUz45jb^})dBTmyG zFF*`HdP7_Y^+E8>z1vQ|J1HI<6M$AuoI0+!bu;TVvF$p?n8UK*WwnGlJP@}$uEW^E zs)6ELXSv%?0r+bn$~)|~4|0B(_^?7X6&!vgy~Xjt>FE)I!^B#~Uq1wt0^a=)CIGE1 zb*u1e#$KperXLo5xkr8LOSqidQJsqk|12l5A||Y+221gM&wSMNcrK^y$9X-Wan0=P z3Jf=xab@Ngug71_^f*|2SkS#-O#<@zA2j0(oq01({o?ibiScKA*To`Z$Bnb=bAqCu zVe|KtE`}{b7Q4v%D~>e7IaD4OonmaPHq{ewFlcT zj09T~ga7CBYSJ?OPl0&Qtc52^YI`mGA0#i)ZOZ|%+W!2DXoeQ_Txf9Ji~s`Nr$N8|92;kev+ zYdk(zmuEGouL32;(xCn&jTIh_|ItZCL$n6oZb3|n?-aT?PNk~~(iGy<;~S_{xk8i% zQ2Q&I5DK}6%yZ&1m^IW6EXbFB?_7}0xHRG~h8!9fl z{_dHRPG6TVZVfk&1bXW0fo&oB`i6DBggAAK`Z~uLN0Yi+F~4aXLk6Wyoy5feFn~K< z+_0(gaG=cRCYU#X!jXIhOi;gqc(ZzC4{mO+*Fh3S{bqGB6){1$Nhckk`g_yA;}YpP zenb}D+)g2Q0Ev`{uo$%(wcnL>E~`^~8vffZV!5Ey4o*CvdlIg?O;Ba$sJrAjV|XKw zA4J`s7Cs|iy5NXPqk$@mvVPik1|t*f25SgA#==jRoPG3Z1+k%1g*XmZUSW5BXq8>C zO{wfNMobQ`vJb=uU!UKP*=6go{abuxzY=#>RrZf?hAWhPHE*!!&phW) z+hg%uFd=ve&LgSYFrJ$vK|fPfgym&f|tz|Nl!? zQ!G&g8`^V`_>p!Q4f(cJds=|SSZdGq^qk#7ll2;Fec|%}hFMeiY*;r;QqXp&sko86 z9NgU^Rp{a?nWv7$ynUoira1b3+d{uFaz;Q5vBp{sBlH&&|JyaA!z;iFx zl(Q>hz7D$HyT=7X)^qJ1mcPqFYDEwZw|y%RnGIUcuU^~tWZDiUK8_|D+8;ls!0y3h z1w0=%2f>r)udzD@yt`K2Jg@H$j|6Yt9Btvf4k+LVDmzI>@%>oaHDnWpvV8q6c{!Y? zO7&6}Ndp4&l%Y*QKpG?TVTTtXk3!MM?AO!VJ>+D( z{r^&C|2-2E-o;z8b2g-QW#$*J$FF939L&@>2VrWc5OlFX;%GIy!r^H3hV+iRM#=WB zlD$*1y`F|@eo*`F{L-sMgG8YAi3a8LPU2cE19Jb7TiuQwX$`PHv?w zQKS8&3hjH%Djo#5I2+D5q}oCQ5ZPS86uPaP_RMsnuNYW5HnuJ|l_tAWsuc9{6_{d&=E&hq$rf^g&T+IXq~qw(zux684o{ z-v)*N!y(%Z7Ig-hs>FFCEN&(#8LUf*yQRrZgT+-E)W2MJHH29jCRRWj3m;%cBqyX8}tUcmhkz_~wZq9{tGyEqI`f%BR$g zSgi8gQYbfsKlBiA1z2pZ4KcafbpGA?RA=uo&Al1OdTjWr>>>?A*ygX@>+3+Kul_87 zCxbY;FkqaB7iUp0JOCx_d^8YxbIq>X@!5Wg)I=Ji((1>HS5rSUGU3o)+C&`gWvf8k z3qXyfKuo4*>?shhqCG)|go?Q++enn9!Ia%}@LHXQr(lF+}ZaM$TGmY@h z)*BSxCYeHaOD8P(C?rhxYUEBadLU%7xpir-0Dl1>T=4MTlUR&24)uL0F1-C2t5Ra?63&1Sdh_M@ix* zG>8!svEoEuem*;UBEtQEQ-|k4z*dpc^>7^HHmIvKFSDsrvbAWXPFO=PvA>*%=<5U9 zv3(-3AK@Ii7%E!IQxk7Ur?lXx#!6DEd>?^+dRUhf~z z^gdVyaz081t^mB(Z=YG?lCISLn7MBCo_alfcKjLMq*cJRx8574&y0~b?c;&Q5Hs(c zD}M8O{qp#;-ykUH_0?yFzyyC59;TXE?ppPCulH|`Khp^f=fULQWKwo9PT%RLEq~XV z+~gLjj^x!w*wEDBFl+7Q?l~Ul@ToD5lkpn6#q7Lt6wie*Chtfu#z7$2 z&V@_~Tck=B8jz?T)C$!Pea=aVp4CV1+j9;SJ0dP2^}7a(DZxxe zV^$_C2lFA^JaCY-z>qr(x+7axu2|O!A;L4d{9vwvIxtiEG7wzfCD}5Q!9A^$$nX)z zkJ1q#a-ei#m^t*nSS_Ps8YX~243AHT-(ND+0tZ0-bryz$)HagqT6bG=!Cy0>B|H6TNLEc zc3hfV+&w34yz8fST_*pXU!DNUb0K~J+tITjU%&daVFLG(2^eZgY1y8Rtlj8w;FC>Q zhl^0kVBS0Pz%jvlUQnX*K>*NKIVF3f4m_by&aA1wdSKRcEWM_*j(z0j;10wU%GBxn z+4;!7ixyhQ z&14idKvgAHg@ZOr|F1D{sX|IYxY^mm5*s_{pBS1xH>KAP1r)r^=*DR%D%T49)vzhx z0AqnT;)ipxynKxLcqs8i1|>Xw^rnH+HXQH9zRUAQdonXxe*~lhknj~?^O zlO0I6L#@GXQTKH$*=C;9k)pbMEZK~6(DXUQ%gIhw^6ysbalssBSxz=S9M)k*+tLQf zM${9Aj9xxg*>Nn{qW##d4tm-Gh5>^cr(^JCYp2!1 zkkem#ojSg4@3u-sR$%0>n92ssxwEvKTs@C&P!J*Veta_7j#>xZX0K5^M;&DO$z%tk zOKlWK;vJ_*U^$k&P(3mCDHLgz79~joi@|uh0kbyb!M0As9L8cDuBZC)ee053C9K=8 z8H4sqU1E<*1Et3Wi}- z(PPO*b@+e~sg?~wR5m6%7@dLt+&oIw!H1e`FZUYlqD4snRvs8PAUnKLo(QsY_uZp* z9QDer;<*M9>MER_LYSuLw-hWXb&F@KaTWD@*cVQD$CB-3XSvCc3dn97788RwgzoVA zcC42tDT?ZR-lNOO^Y|AAfo4pRg1upHWo-G@PPk~l-gkMjlk^76X)XW}#j037de!b^ zM>}d4K_yUYSrHV2Y~vG_Dk_pRlIx7_h{tiEjatI3rYPsj$u7Pr6Tq8A#8k08U2Ml@ zJfC{WzFPp#9i917rh7hd-M`dyPiaq-MXFSx8g=Jo3^0Ug9(o1&Zs~?|pdu*U%lB?1Bn>J}gCfvRHw>Kgg3 zz7}yFF67Zifc8cl@1s8$S0@Nj0?GnHv_8{oV~8D?om^6{*}w&0PYVI%dEkKN3s;j- zppa|kH7VzSv83c5Cq|Y8l;MSSpkPx90%Pnd;jJ@8I-Tl8ZL!L%cd}>hIGjxLil<#j zu85jvv4by(+bH;aj>v;zPDRUu)Y1DQk1Z|`M|yL~X2o_QBI8x`wu6Ye#cqf!HLx*2 zVJFMbV*23A7z_s<$o7+~Q%@zchmXRnlz{df8e1#uh=EqT7ja<8zAAiM1V4`BO5@s5el+>@ z3_m{3kB{NQRY$9RL!O~xLhYmcSVA6t?JPcYmVWJn^0y>S*6v4INNqDbEVT{%IM0ta z@Z-e<#!cjrDFvZ6gGaq&3xOxMPEz0s_$~I<^s3$^c%H$R5;U62J{sJ%#@1oFuR zU693GkcC{R-Nfg;O2FB2pDSBslPj-NJG^Xn_~ZObcKGewdfDMubDL#{zsA30hmUic zWrx4SzhsB6vpal0|KbkU5^nts+_abRt8DXw{7bg^P5eu?`7``WwwV{Qt8KoEf5|rU z+7{Vnsf}L?xvwlRtM>Z;@GseG7T1!!mg*k07jk!b98UJU_CEggMn2<72(|Zj@-NwY zCe5k6XC8snEtCTSinR-HTX5Lej>pvk6mZ#v0 zjk#9cvQBw;ZRVkUdrPXvEoM|{bakqKXeOQRGUUvzY+w2pD2NcYC0-l>Zj)>+wd!@n z!k})UsIz+hPT+X$%UJb&8{)SvpMU@n=S@+26*%w&DqihSIXvdE)eb8gDjOwv*d0dP zdq+>D)FKrJ0*c3(1I6DYVHa0EYQB)Sp1{JOCSQEwKj}taeBwQR9_10BzHk1otzD~x zlIv@?p+!F}T(CGyHfX)@@?j<9Z?K4OqHS23X<9!1~@nz#=N~_Q$kvov-*XBpxQkM zN%A}u>frRULGJ^p*dha@?;9CNOM`$!nKT}d%xPZ@q<_2!K%!_I4M=Q}0n)oi2Ga5% zAW>M42c*o&x2-U;`xD(jc9YlKk3Llgs{=zIl-Do=vi~wN$UZy>WK>ti1DSDsYP`B1 qAd8Z#t%+sWyojBx_OU3fTXh5%+VleTRa|ZcD$)iMd0%g?@c#kN3-ihV literal 52373 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 GRr~)^0pObe diff --git a/_sources/deployment.rst.txt b/_sources/deployment.rst.txt index f30b59c..7644ceb 100644 --- a/_sources/deployment.rst.txt +++ b/_sources/deployment.rst.txt @@ -1,34 +1,32 @@ Deployment ========== -Responder applications are standard ASGI apps. You can deploy them anywhere -you'd deploy a Python web service. +Responder applications are standard `ASGI `_ +apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor +to WSGI — it supports async, WebSockets, and HTTP/2. This means you can +deploy a Responder app anywhere that runs Python, using any ASGI server. Running Locally --------------- -The simplest way to run your application:: - - # api.py - import responder - - api = responder.API() - - @api.route("/") - def hello(req, resp): - resp.text = "hello, world!" +During development, ``api.run()`` is all you need:: if __name__ == "__main__": api.run() -This starts a production uvicorn server on ``127.0.0.1:5042``. +This starts a `uvicorn `_ server on +``127.0.0.1:5042``. Uvicorn is a lightning-fast ASGI server built on +`uvloop `_ — it handles thousands of +concurrent connections efficiently and protects against slowloris attacks, +making a reverse proxy like nginx optional for many deployments. Docker ------ -A minimal Dockerfile for deploying a Responder application:: +Docker is the most common way to package and deploy web applications. +Here's a minimal Dockerfile:: FROM python:3.13-slim WORKDIR /app @@ -43,44 +41,63 @@ Build and run:: $ docker build -t myapi . $ docker run -p 8000:80 myapi +The ``python:3.13-slim`` image is about 150MB — small enough for fast +deploys but includes everything you need. For even smaller images, you +can use ``python:3.13-alpine``, though some packages may need extra +build dependencies. + Cloud Platforms --------------- -Responder automatically honors the ``PORT`` environment variable, which is -set by most cloud platforms. When ``PORT`` is set, Responder binds to -``0.0.0.0`` on that port automatically. +Responder automatically honors the ``PORT`` environment variable. When +``PORT`` is set, the server binds to ``0.0.0.0`` on that port — this is +the convention that virtually every cloud platform uses. -This works out of the box with: +This means zero configuration on: -- **Fly.io** -- **Railway** -- **Render** -- **Google Cloud Run** -- **Azure Container Apps** -- **AWS App Runner** +- **Fly.io** — ``fly launch`` and you're done +- **Railway** — push your code, Railway sets ``PORT`` +- **Render** — set start command to ``python api.py`` +- **Google Cloud Run** — containerize and deploy +- **Azure Container Apps** — same pattern +- **AWS App Runner** — and here too -Just deploy your code and set the start command to ``python api.py``. +The pattern is always the same: deploy your code, set the start command +to ``python api.py``, and the platform handles the rest. Uvicorn Directly ---------------- -For more control over the production server, you can bypass ``api.run()`` -and use uvicorn directly:: +For production deployments where you want more control, bypass +``api.run()`` and use uvicorn directly:: $ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4 -This gives you access to all of uvicorn's options: worker count, SSL -certificates, access logging, and more. See the -`uvicorn documentation `_ for details. +The ``--workers`` flag spawns multiple processes, each handling requests +independently. A good starting point is 2-4 workers per CPU core. + +Uvicorn supports many options — SSL certificates, access logging, graceful +shutdown timeouts, and more. See the +`uvicorn documentation `_ for details. Reverse Proxy ------------- -In production, you may want to place Responder behind a reverse proxy like -nginx or Caddy for SSL termination, load balancing, or serving static assets. +For high-traffic production deployments, you may want a reverse proxy like +`nginx `_ or `Caddy `_ in +front of your application for: + +- **SSL/TLS termination** — let the proxy handle HTTPS certificates +- **Load balancing** — distribute traffic across multiple app instances +- **Static asset serving** — offload static files to the proxy +- **Rate limiting** — at the infrastructure level Responder's ``TrustedHostMiddleware`` and ``HTTPSRedirectMiddleware`` work -correctly behind proxies that set standard forwarding headers. +correctly behind proxies that set standard forwarding headers +(``X-Forwarded-For``, ``X-Forwarded-Proto``). + +That said, uvicorn is production-ready on its own. Many applications run +uvicorn directly without a reverse proxy and do just fine. diff --git a/_sources/quickstart.rst.txt b/_sources/quickstart.rst.txt index f675fc3..1781eed 100644 --- a/_sources/quickstart.rst.txt +++ b/_sources/quickstart.rst.txt @@ -2,164 +2,229 @@ Quick Start =========== This guide will walk you through the basics of building a web service with -Responder. By the end, you'll know how to declare routes, handle requests, -send responses, render templates, and process background tasks. +Responder. By the end, you'll understand how HTTP requests and responses +work, how to define routes, read data from clients, send data back, render +HTML templates, and process work in the background. Create a Web Service -------------------- -The first thing you need to do is declare a web service. This is the central -object that holds all your routes, middleware, and configuration:: +Every web application starts with a single object — the application +instance. In Responder, this is the ``API`` class. It holds your routes, +middleware, templates, and configuration. Think of it as the central +nervous system of your web service:: import responder api = responder.API() +That's it. One import, one line. You now have a fully functional ASGI +application with gzip compression, static file serving, session support, +and a production-ready server — all wired up and ready to go. + Hello World ----------- -Next, add a route. Here, we'll make the root URL say "hello, world!":: +A web service isn't very useful until it can respond to requests. In HTTP, +a *route* maps a URL path to a function that handles it. When a client +(like a browser or ``curl``) sends a request to that path, your function +runs and produces a response. + +Here's the simplest possible route:: @api.route("/") def hello_world(req, resp): resp.text = "hello, world!" -Every view receives a ``req`` (request) and ``resp`` (response) object. You -don't need to return anything — just mutate the response directly. +Two things to notice: + +1. Every view function receives two arguments: ``req`` (the incoming + request) and ``resp`` (the outgoing response). +2. You don't return anything. Instead, you *mutate* the response object + directly. This is a deliberate design choice — it keeps the API + consistent whether you're setting text, JSON, headers, cookies, or + status codes. Run the Server -------------- -Start your web service with ``api.run()``:: +Start your web service with a single call:: api.run() -This spins up a production-grade uvicorn server on port ``5042``, ready for -incoming HTTP requests. +This spins up a production-grade `uvicorn `_ +server on port ``5042``, ready for incoming HTTP requests. Open +``http://localhost:5042`` in your browser and you'll see your hello world +response. You can customize the port with ``api.run(port=8000)``. The ``PORT`` environment variable is also honored automatically — when set, Responder -binds to ``0.0.0.0`` on that port, which is what cloud platforms like -Fly.io, Railway, and Google Cloud Run expect. +binds to ``0.0.0.0`` on that port, which is what cloud platforms expect. .. note:: Both sync and async views are supported. The ``async`` keyword is always - optional — use it when you need to ``await`` something. + optional — use it when you need to ``await`` something, like reading a + request body or querying a database. Route Parameters ---------------- -If you want dynamic URLs, use Python's familiar f-string syntax to declare -variables in your routes:: +Static URLs like ``/about`` are useful, but most applications need dynamic +routes — URLs that contain variable data, like a user ID or a product slug. + +In Responder, you declare route parameters using Python's f-string syntax:: @api.route("/hello/{who}") def hello_to(req, resp, *, who): resp.text = f"hello, {who}!" A ``GET`` request to ``/hello/world`` will respond with ``hello, world!``. +A request to ``/hello/guido`` will respond with ``hello, guido!``. -Route parameters are passed as keyword-only arguments (after the ``*``). +Route parameters are passed as *keyword-only* arguments (after the ``*`` +in the function signature). This is a Python feature that makes the +interface explicit — you always know which arguments come from the URL. Type Convertors ^^^^^^^^^^^^^^^ -You can constrain route parameters to specific types. The parameter will be -automatically converted before it reaches your view:: +By default, route parameters are strings. But often you want them as +integers, UUIDs, or other types. Responder can convert them automatically +using type annotations in the route pattern:: @api.route("/add/{a:int}/{b:int}") async def add(req, resp, *, a, b): resp.text = f"{a} + {b} = {a + b}" +Here, ``a`` and ``b`` will arrive as Python ``int`` objects, not strings. +If someone requests ``/add/3/hello``, they'll get a 404 — the route won't +match because ``hello`` isn't a valid integer. + Supported types: -- ``str`` — matches any string without slashes (default) -- ``int`` — matches digits, converts to ``int`` -- ``float`` — matches decimal numbers, converts to ``float`` +- ``str`` — matches any string without slashes (this is the default) +- ``int`` — matches digits and converts to ``int`` +- ``float`` — matches decimal numbers and converts to ``float`` - ``uuid`` — matches UUID strings like ``550e8400-e29b-41d4-a716-446655440000`` - ``path`` — matches any string *including* slashes, useful for file paths + like ``/files/{filepath:path}`` Sending Responses ----------------- -Responder gives you several ways to send data back to the client. Just set -the appropriate property on the response object. +When an HTTP server receives a request, it must send back a response. Every +HTTP response has three parts: a status code (like ``200 OK`` or ``404 Not +Found``), headers (metadata like ``Content-Type``), and a body (the actual +data). -**Text and HTML**:: +Responder lets you set all three by mutating the response object. + +**Text and HTML** — the simplest response types. ``resp.text`` sets the +``Content-Type`` to ``text/plain``, while ``resp.html`` sets it to +``text/html``:: resp.text = "plain text response" resp.html = "

HTML response

" -**JSON** — the most common pattern for APIs. Set ``resp.media`` to any -JSON-serializable Python object:: +**JSON** — the lingua franca of web APIs. Set ``resp.media`` to any +JSON-serializable Python object — a dict, a list, whatever — and Responder +will serialize it to JSON and set the right headers:: @api.route("/hello/{who}/json") def hello_json(req, resp, *, who): resp.media = {"hello": who} If the client sends an ``Accept: application/x-yaml`` header, the same data -will be returned as YAML instead. Content negotiation is automatic. +will be returned as YAML instead. This is called *content negotiation* — +the server and client agree on a format. It happens automatically. -**Files** — serve a file from disk with automatic content-type detection:: +**Files** — serve a file from disk. Responder uses Python's ``mimetypes`` +module to figure out the ``Content-Type`` from the file extension:: resp.file("reports/annual.pdf") -**Raw bytes**:: +**Raw bytes** — for binary data like images or protocol buffers:: resp.content = b"\x89PNG\r\n..." -**Status codes and headers**:: +**Status codes** — HTTP status codes tell the client what happened. ``200`` +means success, ``201`` means something was created, ``404`` means not found, +``500`` means the server broke. Set it directly:: resp.status_code = 201 + +**Headers** — HTTP headers carry metadata. Common ones include +``Content-Type``, ``Cache-Control``, ``Authorization``, and custom +application headers:: + resp.headers["X-Custom"] = "value" -**Redirects**:: +**Redirects** — tell the client to go somewhere else:: api.redirect(resp, location="/new-url") +This sends a ``301 Moved Permanently`` response by default. The client's +browser will automatically follow the redirect. + Reading Requests ---------------- -The request object gives you access to everything the client sent. +The other half of HTTP is the request — the data the client sends to your +server. This includes the HTTP method (GET, POST, PUT, DELETE), the URL, +headers, query parameters, cookies, and optionally a body. -**Method and URL**:: +Responder wraps all of this in the ``req`` object. + +**Method and URL** — every HTTP request has a method (what the client wants +to do) and a URL (what resource it's about):: req.method # "get", "post", etc. (lowercase) req.full_url # "http://example.com/path?q=1" req.url # parsed URL object -**Headers** — case-insensitive, just like you'd expect:: +**Headers** — HTTP headers carry metadata from the client, like what +content types it accepts, authentication tokens, and more. Responder's +headers dict is case-insensitive, because the HTTP spec says header names +are case-insensitive:: req.headers["Content-Type"] req.headers["content-type"] # same thing -**Query parameters**:: +**Query parameters** — the part of the URL after the ``?``. These are +commonly used for search, filtering, and pagination:: # GET /search?q=python&page=2 req.params["q"] # "python" req.params["page"] # "2" -**Path parameters** — also available on the request object:: +Note that query parameters are always strings. If you need an integer, +you'll need to convert it yourself: ``int(req.params["page"])``. + +**Path parameters** — the dynamic parts of the URL that matched your route +pattern. These are also available on the request object, which is useful +in before-request hooks where they aren't passed as function arguments:: req.path_params["user_id"] # same as the keyword argument -**Request body** — for POST/PUT/PATCH requests, you need to ``await`` the -body content:: +**Request body** — for POST, PUT, and PATCH requests, the client sends +data in the body. Since reading the body is an I/O operation, you need to +``await`` it:: - # JSON body + # JSON body (the most common format for APIs) data = await req.media() - # Form data + # Form data (from HTML forms) data = await req.media("form") - # File uploads + # File uploads (multipart) files = await req.media("files") # Raw bytes @@ -170,41 +235,59 @@ body content:: **Other useful properties**:: - req.is_json # True if content type is JSON - req.cookies # dict of cookies - req.session # session data (dict) - req.client # (host, port) tuple - req.is_secure # True if HTTPS + req.is_json # True if the content type is JSON + req.cookies # dict of cookies sent by the client + req.session # session data (a signed, server-side dict) + req.client # (host, port) tuple — the client's IP address + req.is_secure # True if the request came over HTTPS Rendering Templates ------------------- -Responder includes built-in `Jinja2 `_ -support. Templates are loaded from the ``templates/`` directory by default. +While APIs typically return JSON, many web applications need to render +HTML pages. Responder includes built-in support for +`Jinja2 `_, one of the most popular +templating engines in the Python ecosystem. -The simplest way is to use ``api.template()``:: +Templates let you write HTML with placeholders that get filled in with +dynamic data. This keeps your presentation logic (HTML) separate from +your application logic (Python) — a pattern called +*separation of concerns*. + +The simplest way to render a template is ``api.template()``. Templates +are loaded from the ``templates/`` directory by default:: @api.route("/hello/{name}/html") def hello_html(req, resp, *, name): resp.html = api.template("hello.html", name=name) -You can also use the ``Templates`` class directly for more control:: +The template file ``templates/hello.html`` might look like:: + +

Hello, {{ name }}!

+ +The ``{{ name }}`` part is a Jinja2 expression — it gets replaced with +the value you passed in. + +You can also use the ``Templates`` class directly for more control over +the template directory and configuration:: from responder.templates import Templates - templates = Templates(directory="templates") + templates = Templates(directory="my_templates") @api.route("/page") def page(req, resp): resp.html = templates.render("page.html", title="Hello") -Async rendering is supported too:: +For applications that need non-blocking template rendering (rare, but +useful under extreme load), async rendering is supported:: templates = Templates(directory="templates", enable_async=True) resp.html = await templates.render_async("page.html", title="Hello") -You can render template strings without a file:: +And for quick one-off templates, you can render a string directly without +a file:: resp.html = api.template_string("Hello, {{ name }}!", name="world") @@ -213,7 +296,13 @@ Background Tasks ---------------- Sometimes you want to accept a request, respond immediately, and do the -actual processing later. Responder makes this easy with background tasks:: +actual processing later. This is a common pattern for operations that take +a long time — sending emails, processing images, updating caches, or +calling slow external APIs. + +Responder makes this easy with background tasks. Decorate any function +with ``@api.background.task`` and it will run in a thread pool, separate +from the request/response cycle:: @api.route("/incoming") async def receive_incoming(req, resp): @@ -227,8 +316,17 @@ actual processing later. Responder makes this easy with background tasks:: process_data(data) - # Respond immediately — processing continues in the background + # This response is sent immediately, while process_data + # continues running in the background. resp.media = {"status": "accepted"} -The ``@api.background.task`` decorator wraps any function to run in a thread -pool. The client gets an immediate response while the work continues. +The client gets an instant response — the heavy lifting happens after. +This is the same pattern used by task queues like Celery, but much simpler +for lightweight use cases where you don't need a full message broker. + +.. note:: + + Background tasks run in threads, not processes. They share memory with + your application, which makes them fast to start but means CPU-intensive + work will block the event loop. For heavy computation, consider a proper + task queue. diff --git a/_sources/testing.rst.txt b/_sources/testing.rst.txt index 0e926af..6271d2d 100644 --- a/_sources/testing.rst.txt +++ b/_sources/testing.rst.txt @@ -279,7 +279,7 @@ Tips ``API()`` configuration (like ``cors=True``), create a new instance in the test rather than sharing the fixture. -- **Use ``api.url_for()``** instead of hard-coded paths. It's a small +- Use ``api.url_for()`` instead of hard-coded paths. It's a small thing, but it makes refactoring painless. - **Test the contract, not the implementation.** Assert on status codes, diff --git a/_sources/tour.rst.txt b/_sources/tour.rst.txt index deef200..962c720 100644 --- a/_sources/tour.rst.txt +++ b/_sources/tour.rst.txt @@ -1,15 +1,27 @@ Feature Tour ============ -This section walks through Responder's features in detail. Each section -includes working code examples you can copy into your application. +This section walks through Responder's features in depth. Each section +explains the concept, shows working code, and explains the design choices +behind it. If you're new to web development, this is a good place to learn +how modern web frameworks work under the hood. Method Filtering ---------------- -By default, a route matches all HTTP methods. If you want to restrict a -route to specific methods, pass the ``methods`` parameter:: +HTTP defines several *methods* (also called verbs) that describe what a +client wants to do with a resource. The most common are: + +- ``GET`` — retrieve data +- ``POST`` — create something new +- ``PUT`` — replace something entirely +- ``PATCH`` — update part of something +- ``DELETE`` — remove something + +By default, a Responder route matches all methods. This is fine for simple +endpoints, but REST APIs typically map different methods to different +operations. Use the ``methods`` parameter to restrict a route:: @api.route("/items", methods=["GET"]) def list_items(req, resp): @@ -20,15 +32,22 @@ route to specific methods, pass the ``methods`` parameter:: data = await req.media() resp.media = {"created": data} -Note the ``check_existing=False`` — this allows you to register multiple -handlers for the same path with different methods. +Note the ``check_existing=False`` — Responder normally prevents you from +registering two routes with the same path (to catch typos). When you +intentionally want multiple handlers for the same path with different +methods, you need to opt in. Class-Based Views ----------------- -For more complex resources, you can use class-based views. Responder will -dispatch to the appropriate method handler based on the HTTP method:: +Function-based views are great for simple endpoints, but sometimes you want +to group related HTTP methods together into a single resource. This is +where class-based views come in — a pattern popularized by +`Falcon `_. + +Responder dispatches to the appropriate method handler based on the HTTP +method:: @api.route("/{greeting}") class GreetingResource: @@ -47,14 +66,19 @@ middleware scoped to a single route. Method-specific handlers (``on_get``, ``on_post``, ``on_put``, ``on_delete``, etc.) are called after. No inheritance required — just define a class with the right method names. +This is simpler than Django's ``View`` classes and more Pythonic than +framework-specific base classes. Lifespan Events --------------- -Modern applications often need to set up resources on startup (database -connections, caches, ML models) and tear them down on shutdown. Responder -supports the lifespan context manager pattern:: +Real applications need to set up resources when they start (database +connection pools, ML models, caches) and tear them down when they stop. +This is called the application *lifespan*. + +The modern approach is the *context manager* pattern, where startup and +shutdown are two halves of the same block:: from contextlib import asynccontextmanager @@ -68,7 +92,11 @@ supports the lifespan context manager pattern:: api = responder.API(lifespan=lifespan) -You can also use the traditional event decorator style:: +Everything before ``yield`` runs at startup. Everything after runs at +shutdown. If startup fails, the server won't start. If shutdown raises, +it's logged but the server still exits. + +The traditional event decorator style also works:: @api.on_event("startup") async def startup(): @@ -78,57 +106,71 @@ You can also use the traditional event decorator style:: async def shutdown(): print("shutting down") -The context manager approach is preferred for new code — it makes the -startup/shutdown relationship explicit and keeps related code together. +The context manager is preferred for new code — it keeps related startup +and shutdown logic together and makes resource cleanup more explicit. Serving Files ------------- -Serve files from disk with automatic content-type detection. Responder -uses Python's ``mimetypes`` module to figure out the right ``Content-Type`` -header for you:: +Web applications often need to serve files — downloads, reports, images. +Responder makes this simple with ``resp.file()``, which reads a file from +disk and sets the ``Content-Type`` header automatically using Python's +``mimetypes`` module:: @api.route("/download") def download(req, resp): resp.file("reports/annual.pdf") -You can override the content type if needed:: +You can override the content type if the automatic detection isn't right:: @api.route("/image") def image(req, resp): resp.file("photos/cat.jpg", content_type="image/jpeg") +For large files, use ``resp.stream_file()`` to avoid loading the entire +file into memory. This streams the file in chunks:: + + @api.route("/export") + def export(req, resp): + resp.stream_file("data/export.csv") + Custom Error Handling --------------------- -By default, unhandled exceptions result in a 500 Internal Server Error. -You can register custom handlers for specific exception types to return -structured error responses:: +In production, you don't want your users to see raw Python tracebacks. +Responder lets you register custom handlers for specific exception types, +so you can return clean, structured error responses:: @api.exception_handler(ValueError) async def handle_value_error(req, resp, exc): resp.status_code = 400 resp.media = {"error": str(exc)} -Now, any route that raises a ``ValueError`` will return a clean 400 response -with a JSON error message instead of a generic 500 page. +Now, any route that raises a ``ValueError`` will return a clean JSON +response with a 400 status code instead of a generic 500 error page. + +This is a common pattern in API development — you define your own exception +classes for different error conditions, register handlers for each, and +your API always returns consistent, machine-readable error responses. Before-Request Hooks -------------------- -Run code before every request. This is useful for logging, adding common -headers, or setting up per-request state:: +Sometimes you need to run the same code before every request — +authentication checks, request logging, adding common headers, or setting +up per-request state. Before-request hooks let you do this without +duplicating code in every route:: @api.route(before_request=True) def add_headers(req, resp): - resp.headers["X-API-Version"] = "3.1" + resp.headers["X-API-Version"] = "3.2" -**Short-circuiting:** If your hook sets ``resp.status_code``, the route -handler will be skipped entirely and the response will be sent immediately. -This is the pattern for authentication guards:: +**Short-circuiting** is the really powerful part. If your hook sets +``resp.status_code``, the route handler is skipped entirely and the +response is sent immediately. This is the pattern for authentication:: @api.route(before_request=True) def auth_check(req, resp): @@ -137,19 +179,36 @@ This is the pattern for authentication guards:: resp.media = {"error": "unauthorized"} If the ``Authorization`` header is missing, the client gets a 401 response -and the actual route handler never runs. +and the actual route handler never runs. This is cleaner than adding +auth checks to every individual route. -WebSocket hooks work the same way:: - @api.before_request(websocket=True) - async def ws_auth(ws): - await ws.accept() +After-Request Hooks +------------------- + +The complement to before-request hooks. After-request hooks run after the +route handler completes but before the response is sent. They're useful +for logging, adding response headers, or any post-processing:: + + @api.after_request() + def log_response(req, resp): + print(f"{req.method} {req.full_url} -> {resp.status_code}") + + @api.after_request() + async def add_timing(req, resp): + resp.headers["X-Served-By"] = "responder" WebSocket Support ----------------- -Responder supports WebSockets for real-time, bidirectional communication:: +HTTP is a request-response protocol — the client asks, the server answers. +But some applications need real-time, bidirectional communication: chat +apps, live dashboards, multiplayer games, collaborative editors. + +`WebSockets `_ solve this by +upgrading an HTTP connection into a persistent, full-duplex channel where +both sides can send messages at any time:: @api.route("/ws", websocket=True) async def websocket(ws): @@ -161,14 +220,54 @@ Responder supports WebSockets for real-time, bidirectional communication:: You can send and receive in multiple formats: -- ``send_text`` / ``receive_text`` — plain text -- ``send_json`` / ``receive_json`` — JSON objects +- ``send_text`` / ``receive_text`` — plain text strings +- ``send_json`` / ``receive_json`` — JSON objects (auto-serialized) - ``send_bytes`` / ``receive_bytes`` — raw binary data +WebSocket routes are marked with ``websocket=True`` in the route decorator. +They receive a ``ws`` object instead of ``req`` and ``resp``. + + +Server-Sent Events (SSE) +------------------------- + +SSE is a simpler alternative to WebSockets for *one-way* real-time +communication — the server pushes events to the client, but the client +can't send messages back. This is perfect for live feeds, progress bars, +notification streams, and AI response streaming. + +Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected +by the browser, and doesn't require any special client-side libraries:: + + @api.route("/events") + async def events(req, resp): + @resp.sse + async def stream(): + for i in range(10): + yield {"data": f"message {i}"} + +On the client side, you consume SSE events with JavaScript's built-in +``EventSource`` API:: + + const source = new EventSource("/events"); + source.onmessage = (event) => { + console.log(event.data); + }; + +Each yielded value can be a string (treated as data) or a dict with the +standard SSE fields:: + + yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"} + yield "simple string message" + GraphQL ------- +`GraphQL `_ is a query language for APIs that lets +clients request exactly the data they need — no more, no less. Instead of +multiple REST endpoints, you define a schema and let clients query it. + Responder includes built-in GraphQL support via `Graphene `_. Set up a full GraphQL endpoint with a single method call:: @@ -183,9 +282,10 @@ with a single method call:: api.graphql("/graphql", schema=graphene.Schema(query=Query)) -Visiting ``/graphql`` in a browser renders the GraphiQL interactive IDE, -where you can explore your schema and test queries. Programmatic clients -can POST JSON queries to the same endpoint. +Visiting ``/graphql`` in a browser renders the +`GraphiQL `_ interactive IDE, where +you can explore your schema, write queries, and see results in real-time. +Programmatic clients can POST JSON queries to the same endpoint. You can access the Responder request and response objects in your resolvers through ``info.context["request"]`` and ``info.context["response"]``. @@ -194,8 +294,12 @@ through ``info.context["request"]`` and ``info.context["response"]``. OpenAPI Documentation --------------------- -Responder can generate an OpenAPI schema and serve interactive API -documentation automatically:: +`OpenAPI `_ (formerly Swagger) is the industry +standard for describing REST APIs. An OpenAPI specification lets you +auto-generate interactive documentation, client libraries, and validation +logic. + +Responder generates OpenAPI specs from your code:: api = responder.API( title="Pet Store", @@ -211,9 +315,11 @@ This gives you: There are three ways to document your endpoints. -**Pydantic models** — the recommended approach for new APIs. Use -``request_model`` and ``response_model`` to annotate your routes, and -Responder will generate the schema automatically:: +**Pydantic models** — the recommended approach. Use ``request_model`` and +``response_model`` to annotate your routes, and Responder generates the +schema automatically. When ``request_model`` is set, request bodies are +also validated automatically — invalid inputs get a ``422`` response with +detailed error messages:: from pydantic import BaseModel @@ -232,19 +338,11 @@ Responder will generate the schema automatically:: data = await req.media() resp.media = {"id": 1, **data} -This generates a full OpenAPI path with ``requestBody`` and ``responses`` -schemas, all linked by ``$ref`` to your Pydantic models in -``components/schemas``. +When ``response_model`` is set, the response is serialized through the +model — extra fields are stripped and types are enforced. -You can also register standalone schemas with the ``@api.schema`` decorator:: - - @api.schema("Pet") - class Pet(BaseModel): - name: str - age: int = 0 - -**YAML docstrings** — inline your OpenAPI spec directly in the docstring. -This gives you full control over every detail:: +**YAML docstrings** — for full control, embed OpenAPI YAML in the +docstring:: @api.route("/pets") def list_pets(req, resp): @@ -258,8 +356,7 @@ This gives you full control over every detail:: """ resp.media = [{"name": "Fido"}] -**Marshmallow schemas** — if you're already using marshmallow for -validation, Responder integrates with it via the apispec plugin:: +**Marshmallow schemas** — if you're already using marshmallow:: from marshmallow import Schema, fields @@ -267,19 +364,43 @@ validation, Responder integrates with it via the apispec plugin:: class PetSchema(Schema): name = fields.Str() -All three approaches can be mixed in the same API. Pydantic models, -marshmallow schemas, and YAML docstrings all contribute to the same -generated OpenAPI specification. +All three approaches can be mixed in the same API. You can choose from +multiple documentation themes: ``swagger_ui`` (default), ``redoc``, +``rapidoc``, or ``elements``. -You can choose from multiple documentation themes: -``swagger_ui`` (default), ``redoc``, ``rapidoc``, or ``elements``. + +Route Groups +------------ + +As your application grows, you'll want to organize routes logically. +Route groups let you share a URL prefix across related endpoints — a +common pattern for API versioning:: + + v1 = api.group("/v1") + + @v1.route("/users") + def list_users(req, resp): + resp.media = [] + + @v1.route("/users/{user_id:int}") + def get_user(req, resp, *, user_id): + resp.media = {"id": user_id} + + v2 = api.group("/v2") + + @v2.route("/users") + def list_users_v2(req, resp): + resp.media = {"users": [], "total": 0} + +This keeps your code organized without affecting the routing logic. Mounting Other Apps ------------------- -Responder can mount any WSGI or ASGI application at a subroute. This means -you can gradually migrate from Flask, or run multiple frameworks side by side:: +Responder can mount any WSGI or ASGI application at a subroute. This is +incredibly useful for gradual migrations — you can run Flask and Responder +side by side, moving routes over one at a time:: from flask import Flask @@ -293,12 +414,17 @@ you can gradually migrate from Flask, or run multiple frameworks side by side:: Requests to ``/flask/`` will be handled by Flask. Everything else goes through Responder. Both WSGI and ASGI apps are supported — Responder -wraps WSGI apps automatically. +wraps WSGI apps in an ASGI adapter automatically. Cookies ------- +`Cookies `_ are +small pieces of data that the server asks the browser to store and send +back with every subsequent request. They're the foundation of sessions, +authentication tokens, and user preferences on the web. + Reading and writing cookies is straightforward:: # Read cookies from the request @@ -307,26 +433,28 @@ Reading and writing cookies is straightforward:: # Set a cookie on the response resp.cookies["hello"] = "world" -For more control over cookie directives, use ``set_cookie``:: +For production use, you'll want to set security directives. The +``httponly`` flag prevents JavaScript from reading the cookie (defending +against XSS attacks), and ``secure`` ensures it's only sent over HTTPS:: resp.set_cookie( "token", value="abc123", - max_age=3600, - secure=True, - httponly=True, + max_age=3600, # expires in 1 hour + secure=True, # HTTPS only + httponly=True, # no JavaScript access path="/", ) -Supported directives: ``key``, ``value``, ``expires``, ``max_age``, -``domain``, ``path``, ``secure``, ``httponly``. - Cookie-Based Sessions --------------------- -Responder has built-in support for signed, cookie-based sessions. Just -read from and write to the ``session`` dictionary:: +Sessions let you store per-user data across multiple requests. Responder's +built-in sessions are cookie-based — the session data is serialized, signed +with your secret key, and stored in a cookie. The signature prevents +tampering: if someone modifies the cookie, the signature won't match and +the data will be rejected:: @api.route("/login") def login(req, resp): @@ -336,13 +464,9 @@ read from and write to the ``session`` dictionary:: def profile(req, resp): resp.media = {"user": req.session.get("username")} -The session data is stored in a cookie called ``Responder-Session``. It's -signed for tamper protection, so you can trust that the data originated -from your server. - .. warning:: - For production use, always set a secret key:: + Always set a secret key in production. The default key is not secret:: api = responder.API(secret_key="your-secret-key-here") @@ -350,141 +474,98 @@ from your server. Static Files ------------ -Static files are served from the ``static/`` directory by default:: +Most web applications serve static assets — CSS stylesheets, JavaScript +files, images, fonts. Responder serves these from the ``static/`` directory +by default:: api = responder.API(static_dir="static", static_route="/static") -Place your CSS, JavaScript, images, and other assets in the ``static/`` -directory and they'll be served automatically. +Place your assets in the ``static/`` directory and they'll be served +automatically at ``/static/style.css``, ``/static/app.js``, etc. -For single-page applications, you can serve ``index.html`` as the default -response for all unmatched routes:: +For single-page applications (React, Vue, Angular), you can serve +``index.html`` as the default response for all unmatched routes:: api.add_route("/", static=True) -You can add additional static directories at runtime:: - - api.static_app.add_directory("extra_assets") - CORS ---- -Enable Cross-Origin Resource Sharing for your API:: +`CORS `_ (Cross- +Origin Resource Sharing) is a security mechanism that controls which +websites can make requests to your API. Browsers enforce this — if your +API is at ``api.example.com`` and your frontend is at ``app.example.com``, +the browser will block requests unless your API explicitly allows it. + +Enable CORS and configure which origins are allowed:: api = responder.API(cors=True, cors_params={ - "allow_origins": ["https://example.com"], + "allow_origins": ["https://app.example.com"], "allow_methods": ["GET", "POST"], "allow_headers": ["*"], "allow_credentials": True, "max_age": 600, }) -The default CORS policy is restrictive — you must explicitly enable the -origins, methods, and headers your frontend needs. +The default policy is restrictive — you must explicitly allow each origin. +Using ``["*"]`` for allow_origins permits any website to call your API, +which is fine for public APIs but not for private ones. HSTS ---- -Force all traffic to HTTPS with a single flag:: +`HSTS `_ +(HTTP Strict Transport Security) tells browsers to always use HTTPS when +communicating with your server. Once a browser sees the HSTS header, it +will refuse to connect over plain HTTP, even if the user types ``http://`` +in the address bar:: api = responder.API(enable_hsts=True) -This adds the ``Strict-Transport-Security`` header and redirects HTTP -requests to HTTPS. - Trusted Hosts ------------- -Protect against HTTP Host header attacks by restricting which hostnames -your application will respond to:: +The ``Host`` header in an HTTP request tells the server which domain name +the client used. Attackers can forge this header to trick your application +into generating URLs to malicious domains (a class of attack called *Host +header injection*). + +Restrict which hostnames your application accepts:: api = responder.API(allowed_hosts=["example.com", "*.example.com"]) -Requests with a ``Host`` header that doesn't match any of the patterns -will receive a 400 Bad Request response. Wildcard domains are supported. - -By default, all hostnames are allowed. - - -Server-Sent Events (SSE) ------------------------- - -Stream real-time updates to the client using Server-Sent Events. This is -great for live feeds, progress updates, and AI streaming responses:: - - @api.route("/events") - async def events(req, resp): - @resp.sse - async def stream(): - for i in range(10): - yield {"data": f"message {i}"} - -Each yielded value can be a string (treated as data) or a dict with -``data``, ``event``, ``id``, and ``retry`` fields:: - - yield {"event": "update", "data": "hello", "id": "1"} - yield "simple string message" - - -Streaming Files ---------------- - -For large files, use ``resp.stream_file()`` to stream the content without -loading the entire file into memory:: - - @api.route("/download") - def download(req, resp): - resp.stream_file("large-dataset.csv") - -For small files where memory isn't a concern, ``resp.file()`` loads the -entire file at once — simpler but less efficient for large files. - - -After-Request Hooks -------------------- - -Run code after every request, useful for logging, adding headers, or -cleanup:: - - @api.after_request() - def log_response(req, resp): - print(f"{req.method} {req.full_url} -> {resp.status_code}") - - -Route Groups ------------- - -Organize related routes with a shared URL prefix. Useful for API versioning -and logical grouping:: - - v1 = api.group("/v1") - - @v1.route("/users") - def list_users(req, resp): - resp.media = [] - - @v1.route("/users/{user_id:int}") - def get_user(req, resp, *, user_id): - resp.media = {"id": user_id} +Requests with unrecognized hosts get a ``400 Bad Request``. Wildcard +patterns are supported. By default, all hostnames are allowed. Request ID ---------- -Auto-generate unique request IDs for tracing and debugging. If the client -sends an ``X-Request-ID`` header, it's forwarded; otherwise a new UUID is -generated:: +In distributed systems, tracing a single request across multiple services +is essential for debugging. Request IDs are unique identifiers attached to +each request — if something goes wrong, you can search your logs for that +ID and find every related event. + +Responder can auto-generate request IDs. If the client sends an +``X-Request-ID`` header (common in microservice architectures), it's +forwarded. Otherwise, a new UUID is generated:: api = responder.API(request_id=True) +The ID appears in the ``X-Request-ID`` response header. + Rate Limiting ------------- -Built-in token bucket rate limiter:: +Rate limiting prevents individual clients from overwhelming your API with +too many requests. It's essential for public APIs, and good practice even +for internal ones. + +Responder includes a built-in token bucket rate limiter:: from responder.ext.ratelimit import RateLimiter @@ -492,17 +573,25 @@ Built-in token bucket rate limiter:: limiter.install(api) When the limit is exceeded, clients receive a ``429 Too Many Requests`` -response with ``Retry-After`` and ``X-RateLimit-Remaining`` headers. +response with a ``Retry-After`` header. Every response includes +``X-RateLimit-Limit`` and ``X-RateLimit-Remaining`` headers so clients +can pace themselves. + +The rate limiter is per-client, keyed by IP address. MessagePack ----------- -In addition to JSON and YAML, Responder supports MessagePack for efficient -binary serialization:: +`MessagePack `_ is a binary serialization format +that's more compact and faster to parse than JSON. It's useful for +high-throughput APIs, IoT devices, and anywhere bandwidth matters. - # Decode MessagePack request body +Responder supports MessagePack alongside JSON and YAML:: + + # Decode a MessagePack request body data = await req.media("msgpack") - # Content negotiation also works — clients can send - # Accept: application/x-msgpack to receive MessagePack responses. +Content negotiation works too — clients can send +``Accept: application/x-msgpack`` to receive MessagePack responses +instead of JSON. diff --git a/deployment.html b/deployment.html index d7354eb..ca40e92 100644 --- a/deployment.html +++ b/deployment.html @@ -42,29 +42,27 @@

Deployment

-

Responder applications are standard ASGI apps. You can deploy them anywhere -you’d deploy a Python web service.

+

Responder applications are standard ASGI +apps. ASGI (Asynchronous Server Gateway Interface) is the modern successor +to WSGI — it supports async, WebSockets, and HTTP/2. This means you can +deploy a Responder app anywhere that runs Python, using any ASGI server.

Running Locally

-

The simplest way to run your application:

-
# api.py
-import responder
-
-api = responder.API()
-
-@api.route("/")
-def hello(req, resp):
-    resp.text = "hello, world!"
-
-if __name__ == "__main__":
+

During development, api.run() is all you need:

+
if __name__ == "__main__":
     api.run()
 
-

This starts a production uvicorn server on 127.0.0.1:5042.

+

This starts a uvicorn server on +127.0.0.1:5042. Uvicorn is a lightning-fast ASGI server built on +uvloop — it handles thousands of +concurrent connections efficiently and protects against slowloris attacks, +making a reverse proxy like nginx optional for many deployments.

Docker

-

A minimal Dockerfile for deploying a Responder application:

+

Docker is the most common way to package and deploy web applications. +Here’s a minimal Dockerfile:

FROM python:3.13-slim
 WORKDIR /app
 COPY . .
@@ -79,40 +77,57 @@ you’d deploy a Python web service.

$ docker run -p 8000:80 myapi
+

The python:3.13-slim image is about 150MB — small enough for fast +deploys but includes everything you need. For even smaller images, you +can use python:3.13-alpine, though some packages may need extra +build dependencies.

Cloud Platforms

-

Responder automatically honors the PORT environment variable, which is -set by most cloud platforms. When PORT is set, Responder binds to -0.0.0.0 on that port automatically.

-

This works out of the box with:

+

Responder automatically honors the PORT environment variable. When +PORT is set, the server binds to 0.0.0.0 on that port — this is +the convention that virtually every cloud platform uses.

+

This means zero configuration on:

    -
  • Fly.io

  • -
  • Railway

  • -
  • Render

  • -
  • Google Cloud Run

  • -
  • Azure Container Apps

  • -
  • AWS App Runner

  • +
  • Fly.iofly launch and you’re done

  • +
  • Railway — push your code, Railway sets PORT

  • +
  • Render — set start command to python api.py

  • +
  • Google Cloud Run — containerize and deploy

  • +
  • Azure Container Apps — same pattern

  • +
  • AWS App Runner — and here too

-

Just deploy your code and set the start command to python api.py.

+

The pattern is always the same: deploy your code, set the start command +to python api.py, and the platform handles the rest.

Uvicorn Directly

-

For more control over the production server, you can bypass api.run() -and use uvicorn directly:

+

For production deployments where you want more control, bypass +api.run() and use uvicorn directly:

$ uvicorn api:api --host 0.0.0.0 --port 8000 --workers 4
 
-

This gives you access to all of uvicorn’s options: worker count, SSL -certificates, access logging, and more. See the -uvicorn documentation for details.

+

The --workers flag spawns multiple processes, each handling requests +independently. A good starting point is 2-4 workers per CPU core.

+

Uvicorn supports many options — SSL certificates, access logging, graceful +shutdown timeouts, and more. See the +uvicorn documentation for details.

Reverse Proxy

-

In production, you may want to place Responder behind a reverse proxy like -nginx or Caddy for SSL termination, load balancing, or serving static assets.

+

For high-traffic production deployments, you may want a reverse proxy like +nginx or Caddy in +front of your application for:

+
    +
  • SSL/TLS termination — let the proxy handle HTTPS certificates

  • +
  • Load balancing — distribute traffic across multiple app instances

  • +
  • Static asset serving — offload static files to the proxy

  • +
  • Rate limiting — at the infrastructure level

  • +

Responder’s TrustedHostMiddleware and HTTPSRedirectMiddleware work -correctly behind proxies that set standard forwarding headers.

+correctly behind proxies that set standard forwarding headers +(X-Forwarded-For, X-Forwarded-Proto).

+

That said, uvicorn is production-ready on its own. Many applications run +uvicorn directly without a reverse proxy and do just fine.

diff --git a/index.html b/index.html index a031b3a..9e76c92 100644 --- a/index.html +++ b/index.html @@ -138,9 +138,12 @@ with — you’re in the right place.

  • Serving Files
  • Custom Error Handling
  • Before-Request Hooks
  • +
  • After-Request Hooks
  • WebSocket Support
  • +
  • Server-Sent Events (SSE)
  • GraphQL
  • OpenAPI Documentation
  • +
  • Route Groups
  • Mounting Other Apps
  • Cookies
  • Cookie-Based Sessions
  • @@ -148,10 +151,6 @@ with — you’re in the right place.

  • CORS
  • HSTS
  • Trusted Hosts
  • -
  • Server-Sent Events (SSE)
  • -
  • Streaming Files
  • -
  • After-Request Hooks
  • -
  • Route Groups
  • Request ID
  • Rate Limiting
  • MessagePack
  • diff --git a/quickstart.html b/quickstart.html index 3488ab0..5c8e60f 100644 --- a/quickstart.html +++ b/quickstart.html @@ -43,146 +43,204 @@

    Quick Start

    This guide will walk you through the basics of building a web service with -Responder. By the end, you’ll know how to declare routes, handle requests, -send responses, render templates, and process background tasks.

    +Responder. By the end, you’ll understand how HTTP requests and responses +work, how to define routes, read data from clients, send data back, render +HTML templates, and process work in the background.

    Create a Web Service

    -

    The first thing you need to do is declare a web service. This is the central -object that holds all your routes, middleware, and configuration:

    +

    Every web application starts with a single object — the application +instance. In Responder, this is the API class. It holds your routes, +middleware, templates, and configuration. Think of it as the central +nervous system of your web service:

    import responder
     
     api = responder.API()
     
    +

    That’s it. One import, one line. You now have a fully functional ASGI +application with gzip compression, static file serving, session support, +and a production-ready server — all wired up and ready to go.

    Hello World

    -

    Next, add a route. Here, we’ll make the root URL say “hello, world!”:

    +

    A web service isn’t very useful until it can respond to requests. In HTTP, +a route maps a URL path to a function that handles it. When a client +(like a browser or curl) sends a request to that path, your function +runs and produces a response.

    +

    Here’s the simplest possible route:

    @api.route("/")
     def hello_world(req, resp):
         resp.text = "hello, world!"
     
    -

    Every view receives a req (request) and resp (response) object. You -don’t need to return anything — just mutate the response directly.

    +

    Two things to notice:

    +
      +
    1. Every view function receives two arguments: req (the incoming +request) and resp (the outgoing response).

    2. +
    3. You don’t return anything. Instead, you mutate the response object +directly. This is a deliberate design choice — it keeps the API +consistent whether you’re setting text, JSON, headers, cookies, or +status codes.

    4. +

    Run the Server

    -

    Start your web service with api.run():

    +

    Start your web service with a single call:

    api.run()
     
    -

    This spins up a production-grade uvicorn server on port 5042, ready for -incoming HTTP requests.

    +

    This spins up a production-grade uvicorn +server on port 5042, ready for incoming HTTP requests. Open +http://localhost:5042 in your browser and you’ll see your hello world +response.

    You can customize the port with api.run(port=8000). The PORT environment variable is also honored automatically — when set, Responder -binds to 0.0.0.0 on that port, which is what cloud platforms like -Fly.io, Railway, and Google Cloud Run expect.

    +binds to 0.0.0.0 on that port, which is what cloud platforms expect.

    Note

    Both sync and async views are supported. The async keyword is always -optional — use it when you need to await something.

    +optional — use it when you need to await something, like reading a +request body or querying a database.

    Route Parameters

    -

    If you want dynamic URLs, use Python’s familiar f-string syntax to declare -variables in your routes:

    +

    Static URLs like /about are useful, but most applications need dynamic +routes — URLs that contain variable data, like a user ID or a product slug.

    +

    In Responder, you declare route parameters using Python’s f-string syntax:

    @api.route("/hello/{who}")
     def hello_to(req, resp, *, who):
         resp.text = f"hello, {who}!"
     
    -

    A GET request to /hello/world will respond with hello, world!.

    -

    Route parameters are passed as keyword-only arguments (after the *).

    +

    A GET request to /hello/world will respond with hello, world!. +A request to /hello/guido will respond with hello, guido!.

    +

    Route parameters are passed as keyword-only arguments (after the * +in the function signature). This is a Python feature that makes the +interface explicit — you always know which arguments come from the URL.

    Type Convertors

    -

    You can constrain route parameters to specific types. The parameter will be -automatically converted before it reaches your view:

    +

    By default, route parameters are strings. But often you want them as +integers, UUIDs, or other types. Responder can convert them automatically +using type annotations in the route pattern:

    @api.route("/add/{a:int}/{b:int}")
     async def add(req, resp, *, a, b):
         resp.text = f"{a} + {b} = {a + b}"
     
    +

    Here, a and b will arrive as Python int objects, not strings. +If someone requests /add/3/hello, they’ll get a 404 — the route won’t +match because hello isn’t a valid integer.

    Supported types:

      -
    • str — matches any string without slashes (default)

    • -
    • int — matches digits, converts to int

    • -
    • float — matches decimal numbers, converts to float

    • +
    • str — matches any string without slashes (this is the default)

    • +
    • int — matches digits and converts to int

    • +
    • float — matches decimal numbers and converts to float

    • uuid — matches UUID strings like 550e8400-e29b-41d4-a716-446655440000

    • -
    • path — matches any string including slashes, useful for file paths

    • +
    • path — matches any string including slashes, useful for file paths +like /files/{filepath:path}

    Sending Responses

    -

    Responder gives you several ways to send data back to the client. Just set -the appropriate property on the response object.

    -

    Text and HTML:

    +

    When an HTTP server receives a request, it must send back a response. Every +HTTP response has three parts: a status code (like 200 OK or 404 Not +Found), headers (metadata like Content-Type), and a body (the actual +data).

    +

    Responder lets you set all three by mutating the response object.

    +

    Text and HTML — the simplest response types. resp.text sets the +Content-Type to text/plain, while resp.html sets it to +text/html:

    resp.text = "plain text response"
     resp.html = "<h1>HTML response</h1>"
     
    -

    JSON — the most common pattern for APIs. Set resp.media to any -JSON-serializable Python object:

    +

    JSON — the lingua franca of web APIs. Set resp.media to any +JSON-serializable Python object — a dict, a list, whatever — and Responder +will serialize it to JSON and set the right headers:

    @api.route("/hello/{who}/json")
     def hello_json(req, resp, *, who):
         resp.media = {"hello": who}
     

    If the client sends an Accept: application/x-yaml header, the same data -will be returned as YAML instead. Content negotiation is automatic.

    -

    Files — serve a file from disk with automatic content-type detection:

    +will be returned as YAML instead. This is called content negotiation — +the server and client agree on a format. It happens automatically.

    +

    Files — serve a file from disk. Responder uses Python’s mimetypes +module to figure out the Content-Type from the file extension:

    resp.file("reports/annual.pdf")
     
    -

    Raw bytes:

    +

    Raw bytes — for binary data like images or protocol buffers:

    resp.content = b"\x89PNG\r\n..."
     
    -

    Status codes and headers:

    +

    Status codes — HTTP status codes tell the client what happened. 200 +means success, 201 means something was created, 404 means not found, +500 means the server broke. Set it directly:

    resp.status_code = 201
    -resp.headers["X-Custom"] = "value"
     
    -

    Redirects:

    +

    Headers — HTTP headers carry metadata. Common ones include +Content-Type, Cache-Control, Authorization, and custom +application headers:

    +
    resp.headers["X-Custom"] = "value"
    +
    +
    +

    Redirects — tell the client to go somewhere else:

    api.redirect(resp, location="/new-url")
     
    +

    This sends a 301 Moved Permanently response by default. The client’s +browser will automatically follow the redirect.

    Reading Requests

    -

    The request object gives you access to everything the client sent.

    -

    Method and URL:

    +

    The other half of HTTP is the request — the data the client sends to your +server. This includes the HTTP method (GET, POST, PUT, DELETE), the URL, +headers, query parameters, cookies, and optionally a body.

    +

    Responder wraps all of this in the req object.

    +

    Method and URL — every HTTP request has a method (what the client wants +to do) and a URL (what resource it’s about):

    req.method      # "get", "post", etc. (lowercase)
     req.full_url    # "http://example.com/path?q=1"
     req.url         # parsed URL object
     
    -

    Headers — case-insensitive, just like you’d expect:

    +

    Headers — HTTP headers carry metadata from the client, like what +content types it accepts, authentication tokens, and more. Responder’s +headers dict is case-insensitive, because the HTTP spec says header names +are case-insensitive:

    req.headers["Content-Type"]
     req.headers["content-type"]  # same thing
     
    -

    Query parameters:

    +

    Query parameters — the part of the URL after the ?. These are +commonly used for search, filtering, and pagination:

    # GET /search?q=python&page=2
     req.params["q"]     # "python"
     req.params["page"]  # "2"
     
    -

    Path parameters — also available on the request object:

    +

    Note that query parameters are always strings. If you need an integer, +you’ll need to convert it yourself: int(req.params["page"]).

    +

    Path parameters — the dynamic parts of the URL that matched your route +pattern. These are also available on the request object, which is useful +in before-request hooks where they aren’t passed as function arguments:

    req.path_params["user_id"]  # same as the keyword argument
     
    -

    Request body — for POST/PUT/PATCH requests, you need to await the -body content:

    -
    # JSON body
    +

    Request body — for POST, PUT, and PATCH requests, the client sends +data in the body. Since reading the body is an I/O operation, you need to +await it:

    +
    # JSON body (the most common format for APIs)
     data = await req.media()
     
    -# Form data
    +# Form data (from HTML forms)
     data = await req.media("form")
     
    -# File uploads
    +# File uploads (multipart)
     files = await req.media("files")
     
     # Raw bytes
    @@ -193,40 +251,56 @@ body content:

    Other useful properties:

    -
    req.is_json     # True if content type is JSON
    -req.cookies     # dict of cookies
    -req.session     # session data (dict)
    -req.client      # (host, port) tuple
    -req.is_secure   # True if HTTPS
    +
    req.is_json     # True if the content type is JSON
    +req.cookies     # dict of cookies sent by the client
    +req.session     # session data (a signed, server-side dict)
    +req.client      # (host, port) tuple — the client's IP address
    +req.is_secure   # True if the request came over HTTPS
     

    Rendering Templates

    -

    Responder includes built-in Jinja2 -support. Templates are loaded from the templates/ directory by default.

    -

    The simplest way is to use api.template():

    +

    While APIs typically return JSON, many web applications need to render +HTML pages. Responder includes built-in support for +Jinja2, one of the most popular +templating engines in the Python ecosystem.

    +

    Templates let you write HTML with placeholders that get filled in with +dynamic data. This keeps your presentation logic (HTML) separate from +your application logic (Python) — a pattern called +separation of concerns.

    +

    The simplest way to render a template is api.template(). Templates +are loaded from the templates/ directory by default:

    @api.route("/hello/{name}/html")
     def hello_html(req, resp, *, name):
         resp.html = api.template("hello.html", name=name)
     
    -

    You can also use the Templates class directly for more control:

    +

    The template file templates/hello.html might look like:

    +
    <h1>Hello, {{ name }}!</h1>
    +
    +
    +

    The {{ name }} part is a Jinja2 expression — it gets replaced with +the value you passed in.

    +

    You can also use the Templates class directly for more control over +the template directory and configuration:

    from responder.templates import Templates
     
    -templates = Templates(directory="templates")
    +templates = Templates(directory="my_templates")
     
     @api.route("/page")
     def page(req, resp):
         resp.html = templates.render("page.html", title="Hello")
     
    -

    Async rendering is supported too:

    +

    For applications that need non-blocking template rendering (rare, but +useful under extreme load), async rendering is supported:

    templates = Templates(directory="templates", enable_async=True)
     resp.html = await templates.render_async("page.html", title="Hello")
     
    -

    You can render template strings without a file:

    +

    And for quick one-off templates, you can render a string directly without +a file:

    resp.html = api.template_string("Hello, {{ name }}!", name="world")
     
    @@ -234,7 +308,12 @@ support. Templates are loaded from the

    Background Tasks

    Sometimes you want to accept a request, respond immediately, and do the -actual processing later. Responder makes this easy with background tasks:

    +actual processing later. This is a common pattern for operations that take +a long time — sending emails, processing images, updating caches, or +calling slow external APIs.

    +

    Responder makes this easy with background tasks. Decorate any function +with @api.background.task and it will run in a thread pool, separate +from the request/response cycle:

    @api.route("/incoming")
     async def receive_incoming(req, resp):
         data = await req.media()
    @@ -247,12 +326,21 @@ actual processing later. Responder makes this easy with background tasks:

    process_data(data) - # Respond immediately — processing continues in the background + # This response is sent immediately, while process_data + # continues running in the background. resp.media = {"status": "accepted"}
    -

    The @api.background.task decorator wraps any function to run in a thread -pool. The client gets an immediate response while the work continues.

    +

    The client gets an instant response — the heavy lifting happens after. +This is the same pattern used by task queues like Celery, but much simpler +for lightweight use cases where you don’t need a full message broker.

    +
    +

    Note

    +

    Background tasks run in threads, not processes. They share memory with +your application, which makes them fast to start but means CPU-intensive +work will block the event loop. For heavy computation, consider a proper +task queue.

    +
    diff --git a/searchindex.js b/searchindex.js index d529729..b92d5d5 100644 --- a/searchindex.js +++ b/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"API Documentation":[[0,null]],"Added":[[2,"added"],[2,"id1"],[2,"id7"],[2,"id8"],[2,"id11"],[2,"id14"],[2,"id19"],[2,"id26"],[2,"id32"],[2,"id33"],[2,"id34"],[2,"id35"],[2,"id36"],[2,"id39"],[2,"id40"],[2,"id42"],[2,"id45"],[2,"id48"],[2,"id49"],[2,"id50"],[2,"id54"]],"After-Request Hooks":[[9,"after-request-hooks"]],"Background Tasks":[[6,"background-tasks"]],"Backlog":[[1,null]],"Before-Request Hooks":[[9,"before-request-hooks"]],"Build JavaScript Application":[[3,"build-javascript-application"]],"CORS":[[9,"cors"]],"Changed":[[2,"changed"],[2,"id6"],[2,"id9"],[2,"id12"],[2,"id15"],[2,"id18"],[2,"id23"],[2,"id24"],[2,"id25"],[2,"id27"],[2,"id28"],[2,"id30"],[2,"id31"],[2,"id37"],[2,"id41"],[2,"id46"],[2,"id47"],[2,"id53"]],"Changelog":[[2,null]],"Class-Based Views":[[9,"class-based-views"]],"Cloud Platforms":[[4,"cloud-platforms"]],"Cookie-Based Sessions":[[9,"cookie-based-sessions"]],"Cookies":[[9,"cookies"]],"Create a Web Service":[[6,"create-a-web-service"]],"Custom Error Handling":[[9,"custom-error-handling"]],"Deployment":[[4,null]],"Deprecated":[[2,"deprecated"]],"Development Sandbox":[[7,null]],"Docker":[[4,"docker"]],"Feature Tour":[[9,null]],"Fixed":[[2,"fixed"],[2,"id2"],[2,"id3"],[2,"id4"],[2,"id5"],[2,"id10"],[2,"id13"],[2,"id16"],[2,"id17"],[2,"id20"],[2,"id21"],[2,"id22"],[2,"id29"],[2,"id38"],[2,"id43"],[2,"id44"],[2,"id51"],[2,"id52"]],"Future Ideas":[[1,"future-ideas"]],"Getting Started":[[8,"getting-started"]],"GraphQL":[[9,"graphql"]],"HSTS":[[9,"hsts"]],"Hello World":[[6,"hello-world"]],"Installation":[[5,"installation"]],"Launch Local File":[[3,"launch-local-file"]],"Launch Module Entrypoint":[[3,"launch-module-entrypoint"]],"Launch Remote File":[[3,"launch-remote-file"]],"Launch with Non-Standard Instance Name":[[3,"launch-with-non-standard-instance-name"]],"Lifespan Events":[[9,"lifespan-events"]],"MessagePack":[[9,"messagepack"]],"Method Filtering":[[9,"method-filtering"]],"Mounting Other Apps":[[9,"mounting-other-apps"]],"OpenAPI Documentation":[[9,"openapi-documentation"]],"Operations":[[7,"operations"]],"Project":[[5,null]],"Quick Start":[[6,null]],"Rate Limiting":[[9,"rate-limiting"]],"Reading Requests":[[6,"reading-requests"]],"Removed":[[2,"removed"]],"Rendering Templates":[[6,"rendering-templates"]],"Request ID":[[9,"request-id"]],"Requests & Responses":[[0,"requests-responses"]],"Responder":[[5,null]],"Responder CLI":[[3,null]],"Reverse Proxy":[[4,"reverse-proxy"]],"Route Groups":[[9,"route-groups"]],"Route Parameters":[[6,"route-parameters"]],"Run the Server":[[6,"run-the-server"]],"Running Locally":[[4,"running-locally"]],"Sending Responses":[[6,"sending-responses"]],"Server-Sent Events (SSE)":[[9,"server-sent-events-sse"]],"Serving Files":[[9,"serving-files"]],"Setup":[[7,"setup"]],"Static Files":[[9,"static-files"]],"Streaming Files":[[9,"streaming-files"]],"Testing":[[8,null]],"Testing Before and After Hooks":[[8,"testing-before-and-after-hooks"]],"Testing Error Handling":[[8,"testing-error-handling"]],"Testing File Uploads":[[8,"testing-file-uploads"]],"Testing Headers and Cookies":[[8,"testing-headers-and-cookies"]],"Testing JSON APIs":[[8,"testing-json-apis"]],"Testing Lifespan Events":[[8,"testing-lifespan-events"]],"Testing Request Validation":[[8,"testing-request-validation"]],"Testing WebSockets":[[8,"testing-websockets"]],"The Idea":[[5,"the-idea"]],"Tips":[[8,"tips"]],"Trusted Hosts":[[9,"trusted-hosts"]],"Type Convertors":[[6,"type-convertors"]],"Unreleased":[[2,"unreleased"]],"User Guide":[[5,null]],"Using Fixtures":[[8,"using-fixtures"]],"Utility Functions":[[0,"utility-functions"]],"Uvicorn Directly":[[4,"uvicorn-directly"]],"Web Service (API) Class":[[0,"module-responder"]],"WebSocket Support":[[9,"websocket-support"]],"What You Get":[[5,"what-you-get"]],"v0.0.1 - 2018-10-12":[[2,"v0-0-1-2018-10-12"]],"v0.0.10 - 2018-10-17":[[2,"v0-0-10-2018-10-17"]],"v0.0.2 - 2018-10-13":[[2,"v0-0-2-2018-10-13"]],"v0.0.3 - 2018-10-13":[[2,"v0-0-3-2018-10-13"]],"v0.0.4 - 2018-10-15":[[2,"v0-0-4-2018-10-15"]],"v0.0.5 - 2018-10-15":[[2,"v0-0-5-2018-10-15"]],"v0.0.6 - 2018-10-16":[[2,"v0-0-6-2018-10-16"]],"v0.0.7 - 2018-10-16":[[2,"v0-0-7-2018-10-16"]],"v0.0.8 - 2018-10-17":[[2,"v0-0-8-2018-10-17"]],"v0.0.9 - 2018-10-17":[[2,"v0-0-9-2018-10-17"]],"v0.1.0 - 2018-10-17":[[2,"v0-1-0-2018-10-17"]],"v0.1.1 - 2018-10-17":[[2,"v0-1-1-2018-10-17"]],"v0.1.2 - 2018-10-18":[[2,"v0-1-2-2018-10-18"]],"v0.1.3 - 2018-10-18":[[2,"v0-1-3-2018-10-18"]],"v0.1.4 - 2018-10-19":[[2,"v0-1-4-2018-10-19"]],"v0.1.5 - 2018-10-20":[[2,"v0-1-5-2018-10-20"]],"v0.1.6 - 2018-10-20":[[2,"v0-1-6-2018-10-20"]],"v0.2.0 - 2018-10-22":[[2,"v0-2-0-2018-10-22"]],"v0.2.1 - 2018-10-23":[[2,"v0-2-1-2018-10-23"]],"v0.2.2 - 2018-10-23":[[2,"v0-2-2-2018-10-23"]],"v0.2.3 - 2018-10-24":[[2,"v0-2-3-2018-10-24"]],"v0.3.0 - 2018-10-24":[[2,"v0-3-0-2018-10-24"]],"v0.3.1 - 2018-10-24":[[2,"v0-3-1-2018-10-24"]],"v0.3.2 - 2018-10-25":[[2,"v0-3-2-2018-10-25"]],"v0.3.3 - 2018-10-25":[[2,"v0-3-3-2018-10-25"]],"v1.0.0 - 2018-10-26":[[2,"v1-0-0-2018-10-26"]],"v1.0.1 - 2018-10-26":[[2,"v1-0-1-2018-10-26"]],"v1.0.2 - 2018-10-27":[[2,"v1-0-2-2018-10-27"]],"v1.0.3 - 2018-10-27":[[2,"v1-0-3-2018-10-27"]],"v1.0.4 - 2018-10-27":[[2,"v1-0-4-2018-10-27"]],"v1.0.5- 2018-10-27":[[2,"v1-0-5-2018-10-27"]],"v1.1.0 - 2018-10-27":[[2,"v1-1-0-2018-10-27"]],"v1.1.1 - 2018-10-29":[[2,"v1-1-1-2018-10-29"]],"v1.1.2 - 2018-11-11":[[2,"v1-1-2-2018-11-11"]],"v1.1.3 - 2019-01-12":[[2,"v1-1-3-2019-01-12"]],"v1.2.0 - 2018-12-29":[[2,"v1-2-0-2018-12-29"]],"v1.3.0 - 2019-02-22":[[2,"v1-3-0-2019-02-22"]],"v1.3.1 - 2019-04-28":[[2,"v1-3-1-2019-04-28"]],"v1.3.2 - 2019-08-15":[[2,"v1-3-2-2019-08-15"]],"v2.0.0 - 2019-09-19":[[2,"v2-0-0-2019-09-19"]],"v2.0.1 - 2019-09-20":[[2,"v2-0-1-2019-09-20"]],"v2.0.2 - 2019-09-20":[[2,"v2-0-2-2019-09-20"]],"v2.0.3 - 2019-09-20":[[2,"v2-0-3-2019-09-20"]],"v2.0.4 - 2019-11-19":[[2,"v2-0-4-2019-11-19"]],"v2.0.5 - 2019-12-15":[[2,"v2-0-5-2019-12-15"]],"v3.0.0 - 2026-03-22":[[2,"v3-0-0-2026-03-22"]]},"docnames":["api","backlog","changes","cli","deployment","index","quickstart","sandbox","testing","tour"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1},"filenames":["api.rst","backlog.md","changes.md","cli.rst","deployment.rst","index.rst","quickstart.rst","sandbox.md","testing.rst","tour.rst"],"indexentries":{"accepts() (responder.request method)":[[0,"responder.Request.accepts",false]],"add_event_handler() (responder.api method)":[[0,"responder.API.add_event_handler",false]],"add_route() (responder.api method)":[[0,"responder.API.add_route",false]],"after_request() (responder.api method)":[[0,"responder.API.after_request",false]],"api (class in responder)":[[0,"responder.API",false]],"apparent_encoding (responder.request property)":[[0,"responder.Request.apparent_encoding",false]],"client (responder.request property)":[[0,"responder.Request.client",false]],"content (responder.request property)":[[0,"responder.Request.content",false]],"content (responder.response attribute)":[[0,"responder.Response.content",false]],"cookies (responder.request property)":[[0,"responder.Request.cookies",false]],"cookies (responder.response attribute)":[[0,"responder.Response.cookies",false]],"encoding (responder.request property)":[[0,"responder.Request.encoding",false]],"exception_handler() (responder.api method)":[[0,"responder.API.exception_handler",false]],"file() (responder.response method)":[[0,"responder.Response.file",false]],"formats (responder.response attribute)":[[0,"responder.Response.formats",false]],"full_url (responder.request property)":[[0,"responder.Request.full_url",false]],"graphql() (responder.api method)":[[0,"responder.API.graphql",false]],"group() (responder.api method)":[[0,"responder.API.group",false]],"headers (responder.request property)":[[0,"responder.Request.headers",false]],"headers (responder.response attribute)":[[0,"responder.Response.headers",false]],"is_100() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_100",false]],"is_200() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_200",false]],"is_300() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_300",false]],"is_400() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_400",false]],"is_500() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_500",false]],"is_json (responder.request property)":[[0,"responder.Request.is_json",false]],"media (responder.response attribute)":[[0,"responder.Response.media",false]],"media() (responder.request method)":[[0,"responder.Request.media",false]],"method (responder.request property)":[[0,"responder.Request.method",false]],"module":[[0,"module-responder",false]],"mount() (responder.api method)":[[0,"responder.API.mount",false]],"on_event() (responder.api method)":[[0,"responder.API.on_event",false]],"params (responder.request property)":[[0,"responder.Request.params",false]],"path_matches_route() (responder.api method)":[[0,"responder.API.path_matches_route",false]],"path_params (responder.request property)":[[0,"responder.Request.path_params",false]],"redirect() (responder.api method)":[[0,"responder.API.redirect",false]],"request (class in responder)":[[0,"responder.Request",false]],"requests (responder.api property)":[[0,"responder.API.requests",false]],"responder":[[0,"module-responder",false]],"response (class in responder)":[[0,"responder.Response",false]],"route() (responder.api method)":[[0,"responder.API.route",false]],"schema() (responder.api method)":[[0,"responder.API.schema",false]],"serve() (responder.api method)":[[0,"responder.API.serve",false]],"session (responder.request property)":[[0,"responder.Request.session",false]],"session (responder.response attribute)":[[0,"responder.Response.session",false]],"session() (responder.api method)":[[0,"responder.API.session",false]],"sse() (responder.response method)":[[0,"responder.Response.sse",false]],"state (responder.request property)":[[0,"responder.Request.state",false]],"status_code (responder.response attribute)":[[0,"responder.Response.status_code",false]],"stream_file() (responder.response method)":[[0,"responder.Response.stream_file",false]],"template() (responder.api method)":[[0,"responder.API.template",false]],"template_string() (responder.api method)":[[0,"responder.API.template_string",false]],"text (responder.request property)":[[0,"responder.Request.text",false]],"url (responder.request property)":[[0,"responder.Request.url",false]],"url_for() (responder.api method)":[[0,"responder.API.url_for",false]]},"objects":{"":[[0,0,0,"-","responder"]],"responder":[[0,1,1,"","API"],[0,1,1,"","Request"],[0,1,1,"","Response"]],"responder.API":[[0,2,1,"","add_event_handler"],[0,2,1,"","add_route"],[0,2,1,"","after_request"],[0,2,1,"","exception_handler"],[0,2,1,"","graphql"],[0,2,1,"","group"],[0,2,1,"","mount"],[0,2,1,"","on_event"],[0,2,1,"","path_matches_route"],[0,2,1,"","redirect"],[0,3,1,"","requests"],[0,2,1,"","route"],[0,2,1,"","schema"],[0,2,1,"","serve"],[0,2,1,"","session"],[0,2,1,"","template"],[0,2,1,"","template_string"],[0,2,1,"","url_for"]],"responder.API.status_codes":[[0,4,1,"","is_100"],[0,4,1,"","is_200"],[0,4,1,"","is_300"],[0,4,1,"","is_400"],[0,4,1,"","is_500"]],"responder.Request":[[0,2,1,"","accepts"],[0,3,1,"","apparent_encoding"],[0,3,1,"","client"],[0,3,1,"","content"],[0,3,1,"","cookies"],[0,3,1,"","encoding"],[0,3,1,"","full_url"],[0,3,1,"","headers"],[0,3,1,"","is_json"],[0,2,1,"","media"],[0,3,1,"","method"],[0,3,1,"","params"],[0,3,1,"","path_params"],[0,3,1,"","session"],[0,3,1,"","state"],[0,3,1,"","text"],[0,3,1,"","url"]],"responder.Response":[[0,5,1,"","content"],[0,5,1,"","cookies"],[0,2,1,"","file"],[0,5,1,"","formats"],[0,5,1,"","headers"],[0,5,1,"","media"],[0,5,1,"","session"],[0,2,1,"","sse"],[0,5,1,"","status_code"],[0,2,1,"","stream_file"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","property","Python property"],"4":["py","function","Python function"],"5":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:method","3":"py:property","4":"py:function","5":"py:attribute"},"terms":{"":[0,2,3,4,5,6,8,9],"0":[3,4,6,9],"1":[0,3,4,6,9],"10":[0,6,9],"100":9,"127":[3,4],"13":[3,4],"16":3,"2":[6,8,9],"200":[3,8,9],"201":[6,8],"2024":3,"25":3,"26":3,"3":[4,5,8,9],"301":0,"308":2,"3600":9,"4":4,"40":2,"400":[0,8,9],"401":9,"41d4":6,"422":8,"429":9,"446655440000":6,"500":[2,8,9],"5042":[3,4,6],"55":3,"550e8400":6,"60":9,"600":[0,9],"8":[0,3],"80":4,"8000":[4,6],"8192":0,"9":[5,8],"99":8,"A":[0,3,4,5,6,9],"But":8,"By":[3,6,8,9],"For":[3,4,8,9],"If":[0,3,5,6,8,9],"In":[3,4,9],"It":[3,5,8,9],"No":[8,9],"One":[5,8],"That":[5,8],"The":[0,2,3,4,6,8,9],"There":[8,9],"To":3,"Will":0,"With":0,"__main__":[4,5,8],"__name__":[4,5,8,9],"_route_for":2,"a2wsgi":2,"a716":6,"abc123":[8,9],"abf":3,"abil":2,"abl":0,"about":8,"abov":[3,5],"accept":[0,2,3,6,8,9],"access":[3,4,6,9],"acm":3,"acquir":[3,7],"activ":7,"actual":[0,6,9],"ad":[1,9],"adapt":3,"add":[0,1,2,6,9],"add_directori":9,"add_event_handl":0,"add_head":9,"add_request_id":0,"add_rout":[0,9],"add_tim":8,"add_vers":8,"addit":[0,9],"address":[0,3],"adher":2,"after":[0,5,6],"after_request":[0,1,8,9],"ag":9,"against":[0,9],"ai":9,"alias":2,"alic":9,"all":[0,2,3,4,6,7,8,9],"alloc":8,"allow":9,"allow_credenti":[0,9],"allow_head":[0,9],"allow_method":[0,9],"allow_origin":[0,9],"allow_origin_regex":0,"allowed_host":[0,9],"alreadi":[0,8,9],"also":[0,2,3,6,8,9],"altern":0,"alwai":[5,6,9],"an":[0,3,6,9],"ani":[0,5,6,8,9],"annot":[5,9],"annual":[6,9],"anyon":5,"anyth":6,"anywher":4,"api":[2,3,4,5,6,9],"apispec":[2,9],"app":[0,2,3,4,5,8],"appar":0,"apparent_encod":0,"appear":5,"append":3,"applic":[0,2,4,5,6,8,9],"approach":9,"appropri":[6,9],"approx":3,"ar":[2,3,4,5,6,8,9],"arg":0,"argument":[0,2,6],"around":[0,5],"arrow":3,"asgi":[0,2,4,5,9],"ask":5,"assert":8,"asset":[3,4,9],"assum":3,"async":[0,1,2,5,6,8,9],"asynccontextmanag":9,"asynchron":2,"attack":9,"attr":3,"attribut":[0,3],"auth":5,"auth_check":9,"authent":9,"author":[3,7,9],"auto":9,"auto_escap":0,"autobuild":7,"automat":[0,4,5,6,8,9],"avail":[3,6],"avoid":8,"aw":[3,4],"await":[0,6,8,9],"az":3,"azur":[3,4],"b":[6,8],"back":[0,5,6],"background":[2,5],"backlog":5,"bad":[8,9],"balanc":4,"base":[0,2,3,5],"base_url":0,"basemodel":[0,8,9],"basic":[3,6],"batteri":5,"battl":5,"becom":0,"been":3,"befor":[5,6],"before_request":[0,1,2,8,9],"beforehand":3,"behind":4,"being":0,"best":5,"better":5,"bidirect":9,"bin":7,"binari":9,"bind":[0,4,6],"blob":3,"block":8,"bodi":[0,6,8,9],"boilerpl":5,"both":[5,6,9],"box":4,"break":8,"bring":5,"broke":8,"browser":[3,7,9],"bucket":[3,9],"bufix":2,"bug":[2,8],"bugfix":2,"build":[2,4,5,6,7],"built":[2,5,6,8,9],"bump":2,"bypass":4,"byte":[0,5,6],"cach":[8,9],"caddi":4,"call":[8,9],"callabl":0,"can":[0,3,4,5,6,8,9],"capac":3,"case":[0,5,6],"cat":9,"cd":7,"ceas":2,"central":6,"certain":0,"certif":4,"chanc":3,"chang":3,"changelog":5,"chardet":0,"check":[7,8],"check_exist":[0,9],"choos":[3,9],"chunk":0,"chunk_siz":0,"ci":2,"circuit":[1,5,9],"class":[2,5,6,8],"clean":[5,9],"cleanup":9,"cli":[2,5],"click":5,"client":[0,5,6,8,9],"clone":7,"close":[8,9],"close_database_connection_pool":0,"cloud":[3,5,6],"cmd":4,"code":[0,2,4,6,7,8,9],"com":[3,6,7,9],"command":[3,4],"common":[3,6,9],"commun":9,"compat":3,"complement":1,"complex":9,"compon":9,"compress":5,"concept":2,"concern":9,"conclud":3,"condit":8,"configur":[2,6,8],"conflict":2,"confus":8,"connect":[0,8,9],"consid":1,"constrain":6,"contact":0,"contain":[0,4],"content":[0,2,3,5,6,8,9],"content_typ":[0,8,9],"context":[5,8,9],"contextlib":9,"continu":6,"contract":8,"contribut":9,"control":[4,6,9],"convert":[2,6],"convertor":5,"cooki":[0,2,5,6],"copi":[4,9],"cor":[0,2,5,8],"core":2,"coroutin":0,"correctli":4,"correspond":3,"cors_param":[0,9],"count":[2,4],"creat":[0,5,7,8,9],"create_item":[0,9],"create_pet":9,"cross":9,"css":9,"csv":9,"curl":3,"current":3,"custom":[0,5,6,8],"d":[4,6,8],"dask":3,"data":[0,2,6,8,9],"databas":[8,9],"dataset":9,"date":3,"debug":[0,2,9],"decim":6,"declar":[2,6],"decod":[2,9],"decor":[0,6,9],"deep":5,"def":[0,4,5,6,8,9],"default":[0,1,2,3,6,8,9],"default_valu":[0,9],"defin":[3,9],"definit":0,"denpend":2,"depend":[2,8],"deploi":[4,5],"deploy":5,"descript":[0,9],"design":3,"detail":[4,9],"detect":[0,5,6,9],"dev":2,"dict":[0,5,6,9],"dictionari":[0,9],"differ":[3,8,9],"digit":6,"direct":[2,9],"directli":[5,6,8,9],"directori":[0,3,6,9],"disabl":8,"disk":[0,6,9],"dispatch":[2,9],"distribut":3,"django":5,"dna":5,"do":[3,6],"doc":[7,8,9],"docker":5,"dockerfil":4,"docs_rout":[0,9],"docstr":9,"document":[2,4,5,7],"doesn":[0,9],"domain":9,"don":[3,6,8],"down":9,"download":9,"dramat":2,"drop":2,"dropbox":3,"dump":2,"dure":8,"dynam":6,"e":0,"e29b":6,"each":[0,8,9],"easi":6,"echo":3,"ecosystem":5,"edit":[3,7],"effect":8,"effici":9,"either":[0,3],"element":[0,9],"els":9,"enabl":[2,9],"enable_async":6,"enable_hst":[0,9],"encod":[0,3],"end":6,"endpoint":[0,2,8,9],"enter":8,"entir":[0,9],"entri":3,"entrypoint":5,"enumer":3,"env":4,"environ":[0,4,6],"eol":2,"error":[0,5],"escap":0,"etc":[6,8,9],"even":3,"event":[0,2,5],"event_typ":0,"everi":[0,5,6,8,9],"everyth":[6,9],"exactli":8,"exampl":[3,6,9],"exc":[0,8,9],"exceed":9,"except":[0,2,5,8,9],"exception_cl":0,"exception_handl":[0,8,9],"exist":[0,2,3,5],"exit":8,"expect":[3,6],"expir":9,"explicit":9,"explicitli":9,"explor":[1,5,9],"expos":4,"expose_head":0,"express":5,"ext":[2,9],"extens":2,"extra":[2,3],"extra_asset":9,"extract":0,"f":[0,5,6,8,9],"fail":8,"failur":8,"falcon":5,"fall":0,"fals":[0,8,9],"familiar":[5,6],"fast":[5,8],"fastapi":5,"favourit":3,"featur":[0,5],"feed":9,"feel":5,"few":3,"fido":9,"field":[0,8,9],"figur":[5,9],"file":[0,2,5,6],"filenam":[0,8],"filesystem":[2,3],"filter":5,"fire":8,"first":[6,9],"fit":5,"fix":7,"fixtur":5,"flag":9,"flask":[5,9],"flask_app":9,"float":[0,6,8],"fly":[4,6],"forc":9,"form":[0,3,6],"format":[0,2,7,9],"forward":[4,5,9],"found":2,"framework":[5,9],"fresh":8,"from":[0,2,3,4,5,6,8,9],"frontend":9,"fsspec":3,"full":[0,2,3,9],"full_url":[0,6,9],"fun":5,"func":0,"function":[5,6],"fuse":3,"g":[0,3],"gc":3,"gener":[0,5,8,9],"get":[0,6,9],"get_us":[0,9],"git":[3,7],"github":[3,7],"give":[3,4,6,8,9],"given":[0,8],"gmt":3,"goe":9,"good":5,"googl":[3,4,6],"grade":6,"gradual":9,"graphen":[0,2,5,9],"graphiql":[2,5,9],"graphql":[0,2,5],"great":[5,9],"greet":[5,9],"greet_world":5,"greetingresourc":9,"group":[0,5],"guard":[5,9],"guid":6,"gzip":5,"h1":6,"ha":[8,9],"hadoop":3,"hand":[0,3],"handl":[5,6],"handle_value_error":[0,9],"handler":[0,3,5,8,9],"happen":5,"hard":8,"have":[0,2,3,8],"hdf":3,"head":3,"header":[0,4,5,6,9],"heavi":6,"hello":[0,3,4,5,8,9],"hello_html":6,"hello_json":6,"hello_to":6,"hello_world":6,"helloworld":3,"help":0,"here":[6,9],"hold":[5,6],"home":5,"honor":[4,6],"hook":[1,5],"host":[0,2,3,4,5,6],"hostnam":9,"how":6,"hst":5,"html":[0,2,5,6,9],"http":[0,2,3,5,6,7,8,9],"httpie":3,"httponli":9,"httpsredirectmiddlewar":4,"httpx":[2,8],"i":[0,2,3,4,5,6,8,9],"id":[0,5],"imag":9,"immedi":[5,6,9],"immut":2,"implement":[3,8],"import":[0,2,3,4,5,6,8,9],"importlib":3,"improv":2,"includ":[3,5,6,7,8,9],"incom":[0,6],"index":[0,9],"info":[0,2,9],"inform":0,"infrastructur":5,"inherit":[3,9],"initi":[0,8],"inlin":9,"input":8,"insensit":[0,5,6],"instal":[0,3,4,7,9],"instanc":[0,5,8],"instead":[3,6,8,9],"int":[0,6,9],"integ":0,"integr":9,"intent":5,"interact":[2,9],"intern":[8,9],"internet":3,"invalid":8,"invoc":3,"invok":3,"io":[4,6],"is_100":0,"is_200":0,"is_300":0,"is_400":0,"is_500":0,"is_class_bas":2,"is_json":[0,6],"is_secur":6,"isn":9,"issu":2,"item":[0,8,9],"itemin":0,"itemout":0,"its":[0,2,3],"javascript":[5,9],"jinja2":[0,6],"joi":5,"jpeg":9,"jpg":9,"json":[0,3,5,6,9],"just":[4,6,8,9],"keep":[2,8,9],"kei":[0,8,9],"kennethreitz":[3,7],"keyword":[0,6],"kind":3,"know":6,"known":0,"kwarg":0,"larg":[5,9],"larger":8,"later":[6,8],"launch":5,"layout":3,"lazili":0,"lead":8,"learn":5,"length":3,"less":9,"librari":8,"licens":0,"lifespan":[0,2,5],"like":[3,4,5,6,8,9],"limit":[1,5],"line":3,"link":9,"list":[0,3,8,9],"list_item":9,"list_pet":9,"list_us":[0,9],"live":9,"ll":[5,6,9],"load":[0,2,3,4,6,9],"local":5,"locat":[0,3,6],"log":[4,9],"log_level":2,"log_respons":9,"logic":[5,9],"login":9,"look":5,"lower":0,"lowercas":6,"mai":4,"main":3,"make":[6,8,9],"manag":[5,8,9],"mani":9,"manual":0,"map":2,"mark":2,"marshmallow":[0,2,9],"match":[0,6,9],"max_ag":[0,9],"mean":9,"media":[0,2,5,6,8,9],"memori":[0,9],"messag":[0,9],"messagepack":5,"method":[0,5,6,8],"middelwar":0,"middlewar":[1,2,6,9],"migrat":[2,9],"mime":0,"mimetyp":9,"min":9,"minim":[3,4],"minimum":2,"minor":2,"miss":[8,9],"mix":9,"ml":9,"mode":[0,7],"model":[0,8,9],"modern":9,"modul":[2,5,9],"more":[3,4,5,6,9],"most":[4,6,8],"mount":[0,2,5],"move":2,"msgpack":9,"much":9,"multipart":2,"multipl":[2,3,9],"must":[0,9],"mutabl":5,"mutat":[0,5,6],"myapi":4,"n":6,"name":[0,5,6,8,9],"namespac":2,"natur":8,"need":[3,5,6,8,9],"negoti":[0,5,6,8,9],"network":8,"never":9,"new":[0,6,8,9],"next":[3,6],"nginx":4,"non":5,"none":[0,2],"notabl":2,"notasecret":0,"note":9,"now":[2,9],"npm":3,"number":6,"object":[0,2,3,5,6,9],"objecttyp":[0,9],"obviou":8,"oci":3,"oct":3,"often":9,"ok":[3,8],"on_delet":9,"on_ev":[0,8,9],"on_get":[5,9],"on_post":[5,9],"on_put":9,"on_request":[5,9],"on_startup":8,"onc":9,"one":[0,3,5],"onli":6,"open":[2,7],"open_database_connection_pool":0,"openapi":[0,2,5],"openapi_rout":0,"openapi_them":0,"option":[0,2,3,4,5,6],"order":3,"organ":[8,9],"origin":9,"other":[0,2,3,5,6],"otherwis":[0,9],"out":[4,5,9],"over":[0,4,5,9],"overal":2,"overhead":8,"overrid":[0,9],"p":4,"packag":[2,3],"page":[2,6,9],"painless":8,"param":[0,2,5,6],"paramet":[0,5,8,9],"parameter":0,"pars":[0,2,6,8],"pass":[0,5,6,8,9],"passion":5,"password":3,"patch":6,"path":[0,2,3,5,6,8,9],"path_matches_rout":0,"path_param":[0,6],"pattern":[5,6,9],"pdf":[6,8,9],"per":[8,9],"period":9,"person":5,"pet":[0,9],"petin":9,"petout":9,"petschema":[0,9],"photo":9,"pin":2,"pip":[3,4,5,7],"place":[4,5,9],"plain":[3,6,9],"platform":[2,5,6],"pleas":3,"pleasant":5,"plugin":[2,9],"point":3,"polici":9,"pool":[5,6],"port":[0,3,4,6,8],"portion":0,"post":[0,6,8,9],"potenti":2,"power":[5,8],"prefer":9,"prefix":[0,9],"price":[0,8],"primari":0,"print":9,"process":[5,6,8],"process_data":6,"product":[4,5,6,8,9],"profil":9,"program":3,"programmat":9,"progress":9,"project":[2,3,7],"propag":8,"properli":8,"properti":[0,6,8],"protect":9,"protocol":[3,8],"prototyp":[2,5],"provid":0,"proxi":5,"push":5,"put":6,"py":[2,3,4,8],"pydant":[0,8,9],"pyproject":2,"pytest":[5,7,8],"python":[0,2,3,4,5,6,9],"q":6,"queri":[0,5,6,9],"quick":5,"r":[6,8],"race":8,"railwai":[4,6],"rais":[2,8,9],"raise_server_except":8,"random":0,"rang":[0,9],"rapidoc":[0,9],"rate":[1,5],"ratelimit":9,"rather":8,"raw":[3,5,6,9],"re":[0,5,8,9],"reach":6,"react":2,"read":[0,2,5,9],"readi":[5,6],"real":9,"realli":8,"receiv":[0,6,8,9],"receive_byt":9,"receive_incom":6,"receive_json":9,"receive_text":[8,9],"recommend":9,"redirect":[0,2,6,9],"redoc":[0,9],"reduc":2,"ref":[3,9],"refactor":[2,8],"refer":[3,8],"reflect":3,"regist":[0,8,9],"regular":3,"reject":8,"relat":9,"relationship":9,"releas":7,"reliabl":8,"remain":9,"remot":5,"renam":8,"render":[0,1,4,5,9],"render_async":6,"replac":2,"report":[6,8,9],"repres":0,"represent":0,"req":[0,4,5,6,8,9],"request":[2,3,5],"request_id":[0,9],"request_model":[0,8,9],"requestbodi":9,"requir":[2,8,9],"research":5,"resolv":[2,9],"resolve_hello":[0,9],"resourc":9,"resp":[0,2,4,5,6,8,9],"respond":[0,2,4,6,7,8,9],"respons":[3,5,8,9],"response_model":[0,9],"rest":5,"restrict":9,"result":9,"resum":2,"resume_incomplet":2,"retri":[0,9],"return":[0,2,5,6,8,9],"revers":5,"rfc3986":2,"right":[5,9],"root":6,"rout":[0,2,4,5,8],"router":2,"ruff":7,"run":[0,2,3,5,7,8,9],"runner":4,"runtim":9,"s3":3,"safe":2,"sai":6,"same":[5,6,9],"sandbox":5,"sat":3,"scale":5,"schema":[0,2,5,9],"scope":[0,9],"scratch":5,"search":[0,2,6],"second":3,"secret":9,"secret_kei":[0,9],"section":[3,9],"secur":9,"see":[3,4,8],"select":[0,2,3],"self":[0,9],"semant":2,"send":[0,2,5,8,9],"send_byt":9,"send_json":9,"send_text":[8,9],"sent":[0,5,6],"separ":8,"sequenti":2,"serial":9,"serializ":6,"serv":[0,2,4,5,6,8],"server":[0,2,3,4,5,8],"servestat":2,"servic":[3,4,5,8],"session":[0,2,5,6,8],"session_id":9,"set":[0,2,4,5,6,7,8,9],"set_cooki":[2,9],"set_text":0,"setup":[2,8],"sever":6,"sftp":3,"share":[0,5,8,9],"short":[1,5,9],"should":5,"shouldn":0,"show":2,"shut":9,"shutdown":[0,5,8,9],"shutdwown":2,"side":9,"sign":[5,9],"simpl":[8,9],"simplecooki":0,"simpler":[5,9],"simplest":[4,6],"simul":6,"sinc":8,"singl":[2,3,5,9],"size":0,"skip":9,"slash":6,"sleep":[6,8],"slim":4,"small":[5,8,9],"smb":3,"so":[3,8,9],"someth":[5,6,8],"sometim":6,"sourc":[0,7],"spec":9,"specif":[0,3,6,8,9],"specifi":3,"sphinx":7,"spin":6,"sse":[0,5],"ssh":3,"ssl":4,"stabil":2,"standalon":9,"standard":[4,5],"starlett":[0,2,5,8],"start":[4,5,9],"startup":[0,2,5,8,9],"state":[0,2,8,9],"statement":5,"static":[0,2,4,5],"static_app":9,"static_dir":[0,9],"static_rout":[0,2,9],"statu":[0,2,6,8],"status_cod":[0,5,6,8,9],"stdlib":2,"stop":9,"storag":3,"store":[0,3,9],"str":[0,6,8,9],"straightforward":9,"stranger":[0,9],"stream":[0,2,5],"stream_fil":[0,9],"strict":9,"string":[0,5,6,9],"structur":9,"style":9,"subcommand":3,"subrout":[5,9],"subtl":2,"suit":8,"suppli":2,"support":[0,1,2,3,5,6],"surpris":3,"suspect":5,"swagger":[5,9],"swagger_ui":[0,9],"switch":2,"symbol":3,"sync":[2,5,6],"syntax":[5,6],"synthet":3,"system":3,"t":[0,3,4,6,8,9],"take":5,"tamper":9,"target":[2,3],"task":[2,5],"tear":9,"templat":[0,1,2,5],"template_str":[0,6],"templates_dir":0,"termin":4,"terms_of_servic":0,"test":[0,2,5,7,9],"test_500":[2,8],"test_api":8,"test_create_item":8,"test_custom_error":8,"test_head":8,"test_hello":8,"test_hook":8,"test_json":8,"test_upload":8,"test_valid":8,"test_websocket":8,"test_with_lifespan":8,"testclient":[0,2,8],"text":[0,3,4,5,6,8,9],"than":[3,8],"thei":9,"them":[4,5,8,9],"theme":[0,9],"thi":[0,2,3,4,6,8,9],"thing":[6,8],"those":3,"thread":[5,6],"threadpoolexecutor":2,"three":9,"through":[6,9],"time":[0,3,5,6,8,9],"time_start":0,"tip":5,"titl":[0,6,9],"togeth":[5,9],"token":9,"toml":2,"too":[6,8,9],"tool":7,"toolbelt":2,"tour":5,"trace":9,"traceback":2,"tradit":9,"traffic":9,"transfer":3,"transport":9,"treat":[0,9],"trigger":8,"true":[0,6,8,9],"trust":5,"trustedhostmiddlewar":4,"tupl":[0,6,8],"type":[0,2,3,5,9],"typic":3,"typo":2,"ui":[2,5,9],"unauthor":9,"unhandl":9,"unicod":0,"uniqu":9,"unknown":0,"unmaintain":2,"unmatch":9,"unpin":2,"up":[0,2,6,7,9],"updat":[2,9],"upgrad":[2,7],"upload":[2,5,6],"url":[0,2,3,5,6,8,9],"url_for":[0,8],"urllib":2,"us":[0,2,3,4,5,6,9],"usag":[0,2],"user":[0,3,9],"user_id":[6,9],"usernam":9,"usual":[5,8],"utf":[0,3],"util":5,"uuid":[0,6,9],"uuid4":0,"uv":[3,5,7],"uvicorn":[0,2,3,5,6],"v1":[0,9],"valid":[3,5,9],"valu":[0,5,6,8,9],"valueerror":[0,8,9],"variabl":[0,4,6],"ve":[5,8],"venv":7,"veri":0,"verifi":8,"version":[0,2,3,8,9],"via":[2,9],"view":[0,2,5,6,8],"virtualenv":7,"visit":9,"w":[8,9],"wa":[0,2],"wai":[4,6,9],"walk":[6,9],"want":[0,4,5,6,8,9],"watch":7,"we":6,"web":[3,4,5],"webdav":3,"websocket":[0,1,2,5],"websocket_connect":8,"well":5,"went":5,"wget":3,"what":[6,8],"when":[2,3,4,5,6,8,9],"where":[3,9],"whether":0,"which":[2,3,4,6,8,9],"whichev":5,"while":[6,9],"whitenois":2,"who":[5,6],"widget":8,"wildcard":9,"window":2,"within":[2,3],"without":[0,6,8,9],"won":8,"work":[3,4,5,6,9],"workdir":4,"worker":4,"workgroup":3,"world":[0,3,4,5,8,9],"worri":8,"would":[3,8],"wrap":[6,8,9],"write":9,"ws_auth":9,"wsgi":[0,2,5,9],"x":[0,6,8,9],"x89png":6,"xml":0,"yaml":[0,2,5,6,8,9],"yield":[0,9],"yml":[0,9],"you":[0,3,4,6,8,9],"your":[3,4,6,8,9]},"titles":["API Documentation","Backlog","Changelog","Responder CLI","Deployment","Responder","Quick Start","Development Sandbox","Testing","Feature Tour"],"titleterms":{"0":2,"01":2,"02":2,"03":2,"04":2,"08":2,"09":2,"1":2,"10":2,"11":2,"12":2,"13":2,"15":2,"16":2,"17":2,"18":2,"19":2,"2":2,"20":2,"2018":2,"2019":2,"2026":2,"22":2,"23":2,"24":2,"25":2,"26":2,"27":2,"28":2,"29":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2,"The":5,"ad":2,"after":[8,9],"api":[0,8],"app":9,"applic":3,"background":6,"backlog":1,"base":9,"befor":[8,9],"build":3,"chang":2,"changelog":2,"class":[0,9],"cli":3,"cloud":4,"convertor":6,"cooki":[8,9],"cor":9,"creat":6,"custom":9,"deploy":4,"deprec":2,"develop":7,"directli":4,"docker":4,"document":[0,9],"entrypoint":3,"error":[8,9],"event":[8,9],"featur":9,"file":[3,8,9],"filter":9,"fix":2,"fixtur":8,"function":0,"futur":1,"get":[5,8],"graphql":9,"group":9,"guid":5,"handl":[8,9],"header":8,"hello":6,"hook":[8,9],"host":9,"hst":9,"id":9,"idea":[1,5],"instal":5,"instanc":3,"javascript":3,"json":8,"launch":3,"lifespan":[8,9],"limit":9,"local":[3,4],"messagepack":9,"method":9,"modul":3,"mount":9,"name":3,"non":3,"openapi":9,"oper":7,"other":9,"paramet":6,"platform":4,"project":5,"proxi":4,"quick":6,"rate":9,"read":6,"remot":3,"remov":2,"render":6,"request":[0,6,8,9],"respond":[3,5],"respons":[0,6],"revers":4,"rout":[6,9],"run":[4,6],"sandbox":7,"send":6,"sent":9,"serv":9,"server":[6,9],"servic":[0,6],"session":9,"setup":7,"sse":9,"standard":3,"start":[6,8],"static":9,"stream":9,"support":9,"task":6,"templat":6,"test":8,"tip":8,"tour":9,"trust":9,"type":6,"unreleas":2,"upload":8,"us":8,"user":5,"util":0,"uvicorn":4,"v0":2,"v1":2,"v2":2,"v3":2,"valid":8,"view":9,"web":[0,6],"websocket":[8,9],"what":5,"world":6,"you":5}}) \ No newline at end of file +Search.setIndex({"alltitles":{"API Documentation":[[0,null]],"Added":[[2,"added"],[2,"id1"],[2,"id7"],[2,"id8"],[2,"id11"],[2,"id14"],[2,"id19"],[2,"id26"],[2,"id32"],[2,"id33"],[2,"id34"],[2,"id35"],[2,"id36"],[2,"id39"],[2,"id40"],[2,"id42"],[2,"id45"],[2,"id48"],[2,"id49"],[2,"id50"],[2,"id54"]],"After-Request Hooks":[[9,"after-request-hooks"]],"Background Tasks":[[6,"background-tasks"]],"Backlog":[[1,null]],"Before-Request Hooks":[[9,"before-request-hooks"]],"Build JavaScript Application":[[3,"build-javascript-application"]],"CORS":[[9,"cors"]],"Changed":[[2,"changed"],[2,"id6"],[2,"id9"],[2,"id12"],[2,"id15"],[2,"id18"],[2,"id23"],[2,"id24"],[2,"id25"],[2,"id27"],[2,"id28"],[2,"id30"],[2,"id31"],[2,"id37"],[2,"id41"],[2,"id46"],[2,"id47"],[2,"id53"]],"Changelog":[[2,null]],"Class-Based Views":[[9,"class-based-views"]],"Cloud Platforms":[[4,"cloud-platforms"]],"Cookie-Based Sessions":[[9,"cookie-based-sessions"]],"Cookies":[[9,"cookies"]],"Create a Web Service":[[6,"create-a-web-service"]],"Custom Error Handling":[[9,"custom-error-handling"]],"Deployment":[[4,null]],"Deprecated":[[2,"deprecated"]],"Development Sandbox":[[7,null]],"Docker":[[4,"docker"]],"Feature Tour":[[9,null]],"Fixed":[[2,"fixed"],[2,"id2"],[2,"id3"],[2,"id4"],[2,"id5"],[2,"id10"],[2,"id13"],[2,"id16"],[2,"id17"],[2,"id20"],[2,"id21"],[2,"id22"],[2,"id29"],[2,"id38"],[2,"id43"],[2,"id44"],[2,"id51"],[2,"id52"]],"Future Ideas":[[1,"future-ideas"]],"Getting Started":[[8,"getting-started"]],"GraphQL":[[9,"graphql"]],"HSTS":[[9,"hsts"]],"Hello World":[[6,"hello-world"]],"Installation":[[5,"installation"]],"Launch Local File":[[3,"launch-local-file"]],"Launch Module Entrypoint":[[3,"launch-module-entrypoint"]],"Launch Remote File":[[3,"launch-remote-file"]],"Launch with Non-Standard Instance Name":[[3,"launch-with-non-standard-instance-name"]],"Lifespan Events":[[9,"lifespan-events"]],"MessagePack":[[9,"messagepack"]],"Method Filtering":[[9,"method-filtering"]],"Mounting Other Apps":[[9,"mounting-other-apps"]],"OpenAPI Documentation":[[9,"openapi-documentation"]],"Operations":[[7,"operations"]],"Project":[[5,null]],"Quick Start":[[6,null]],"Rate Limiting":[[9,"rate-limiting"]],"Reading Requests":[[6,"reading-requests"]],"Removed":[[2,"removed"]],"Rendering Templates":[[6,"rendering-templates"]],"Request ID":[[9,"request-id"]],"Requests & Responses":[[0,"requests-responses"]],"Responder":[[5,null]],"Responder CLI":[[3,null]],"Reverse Proxy":[[4,"reverse-proxy"]],"Route Groups":[[9,"route-groups"]],"Route Parameters":[[6,"route-parameters"]],"Run the Server":[[6,"run-the-server"]],"Running Locally":[[4,"running-locally"]],"Sending Responses":[[6,"sending-responses"]],"Server-Sent Events (SSE)":[[9,"server-sent-events-sse"]],"Serving Files":[[9,"serving-files"]],"Setup":[[7,"setup"]],"Static Files":[[9,"static-files"]],"Testing":[[8,null]],"Testing Before and After Hooks":[[8,"testing-before-and-after-hooks"]],"Testing Error Handling":[[8,"testing-error-handling"]],"Testing File Uploads":[[8,"testing-file-uploads"]],"Testing Headers and Cookies":[[8,"testing-headers-and-cookies"]],"Testing JSON APIs":[[8,"testing-json-apis"]],"Testing Lifespan Events":[[8,"testing-lifespan-events"]],"Testing Request Validation":[[8,"testing-request-validation"]],"Testing WebSockets":[[8,"testing-websockets"]],"The Idea":[[5,"the-idea"]],"Tips":[[8,"tips"]],"Trusted Hosts":[[9,"trusted-hosts"]],"Type Convertors":[[6,"type-convertors"]],"Unreleased":[[2,"unreleased"]],"User Guide":[[5,null]],"Using Fixtures":[[8,"using-fixtures"]],"Utility Functions":[[0,"utility-functions"]],"Uvicorn Directly":[[4,"uvicorn-directly"]],"Web Service (API) Class":[[0,"module-responder"]],"WebSocket Support":[[9,"websocket-support"]],"What You Get":[[5,"what-you-get"]],"v0.0.1 - 2018-10-12":[[2,"v0-0-1-2018-10-12"]],"v0.0.10 - 2018-10-17":[[2,"v0-0-10-2018-10-17"]],"v0.0.2 - 2018-10-13":[[2,"v0-0-2-2018-10-13"]],"v0.0.3 - 2018-10-13":[[2,"v0-0-3-2018-10-13"]],"v0.0.4 - 2018-10-15":[[2,"v0-0-4-2018-10-15"]],"v0.0.5 - 2018-10-15":[[2,"v0-0-5-2018-10-15"]],"v0.0.6 - 2018-10-16":[[2,"v0-0-6-2018-10-16"]],"v0.0.7 - 2018-10-16":[[2,"v0-0-7-2018-10-16"]],"v0.0.8 - 2018-10-17":[[2,"v0-0-8-2018-10-17"]],"v0.0.9 - 2018-10-17":[[2,"v0-0-9-2018-10-17"]],"v0.1.0 - 2018-10-17":[[2,"v0-1-0-2018-10-17"]],"v0.1.1 - 2018-10-17":[[2,"v0-1-1-2018-10-17"]],"v0.1.2 - 2018-10-18":[[2,"v0-1-2-2018-10-18"]],"v0.1.3 - 2018-10-18":[[2,"v0-1-3-2018-10-18"]],"v0.1.4 - 2018-10-19":[[2,"v0-1-4-2018-10-19"]],"v0.1.5 - 2018-10-20":[[2,"v0-1-5-2018-10-20"]],"v0.1.6 - 2018-10-20":[[2,"v0-1-6-2018-10-20"]],"v0.2.0 - 2018-10-22":[[2,"v0-2-0-2018-10-22"]],"v0.2.1 - 2018-10-23":[[2,"v0-2-1-2018-10-23"]],"v0.2.2 - 2018-10-23":[[2,"v0-2-2-2018-10-23"]],"v0.2.3 - 2018-10-24":[[2,"v0-2-3-2018-10-24"]],"v0.3.0 - 2018-10-24":[[2,"v0-3-0-2018-10-24"]],"v0.3.1 - 2018-10-24":[[2,"v0-3-1-2018-10-24"]],"v0.3.2 - 2018-10-25":[[2,"v0-3-2-2018-10-25"]],"v0.3.3 - 2018-10-25":[[2,"v0-3-3-2018-10-25"]],"v1.0.0 - 2018-10-26":[[2,"v1-0-0-2018-10-26"]],"v1.0.1 - 2018-10-26":[[2,"v1-0-1-2018-10-26"]],"v1.0.2 - 2018-10-27":[[2,"v1-0-2-2018-10-27"]],"v1.0.3 - 2018-10-27":[[2,"v1-0-3-2018-10-27"]],"v1.0.4 - 2018-10-27":[[2,"v1-0-4-2018-10-27"]],"v1.0.5- 2018-10-27":[[2,"v1-0-5-2018-10-27"]],"v1.1.0 - 2018-10-27":[[2,"v1-1-0-2018-10-27"]],"v1.1.1 - 2018-10-29":[[2,"v1-1-1-2018-10-29"]],"v1.1.2 - 2018-11-11":[[2,"v1-1-2-2018-11-11"]],"v1.1.3 - 2019-01-12":[[2,"v1-1-3-2019-01-12"]],"v1.2.0 - 2018-12-29":[[2,"v1-2-0-2018-12-29"]],"v1.3.0 - 2019-02-22":[[2,"v1-3-0-2019-02-22"]],"v1.3.1 - 2019-04-28":[[2,"v1-3-1-2019-04-28"]],"v1.3.2 - 2019-08-15":[[2,"v1-3-2-2019-08-15"]],"v2.0.0 - 2019-09-19":[[2,"v2-0-0-2019-09-19"]],"v2.0.1 - 2019-09-20":[[2,"v2-0-1-2019-09-20"]],"v2.0.2 - 2019-09-20":[[2,"v2-0-2-2019-09-20"]],"v2.0.3 - 2019-09-20":[[2,"v2-0-3-2019-09-20"]],"v2.0.4 - 2019-11-19":[[2,"v2-0-4-2019-11-19"]],"v2.0.5 - 2019-12-15":[[2,"v2-0-5-2019-12-15"]],"v3.0.0 - 2026-03-22":[[2,"v3-0-0-2026-03-22"]]},"docnames":["api","backlog","changes","cli","deployment","index","quickstart","sandbox","testing","tour"],"envversion":{"sphinx":65,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2,"sphinx.ext.viewcode":1},"filenames":["api.rst","backlog.md","changes.md","cli.rst","deployment.rst","index.rst","quickstart.rst","sandbox.md","testing.rst","tour.rst"],"indexentries":{"accepts() (responder.request method)":[[0,"responder.Request.accepts",false]],"add_event_handler() (responder.api method)":[[0,"responder.API.add_event_handler",false]],"add_route() (responder.api method)":[[0,"responder.API.add_route",false]],"after_request() (responder.api method)":[[0,"responder.API.after_request",false]],"api (class in responder)":[[0,"responder.API",false]],"apparent_encoding (responder.request property)":[[0,"responder.Request.apparent_encoding",false]],"client (responder.request property)":[[0,"responder.Request.client",false]],"content (responder.request property)":[[0,"responder.Request.content",false]],"content (responder.response attribute)":[[0,"responder.Response.content",false]],"cookies (responder.request property)":[[0,"responder.Request.cookies",false]],"cookies (responder.response attribute)":[[0,"responder.Response.cookies",false]],"encoding (responder.request property)":[[0,"responder.Request.encoding",false]],"exception_handler() (responder.api method)":[[0,"responder.API.exception_handler",false]],"file() (responder.response method)":[[0,"responder.Response.file",false]],"formats (responder.response attribute)":[[0,"responder.Response.formats",false]],"full_url (responder.request property)":[[0,"responder.Request.full_url",false]],"graphql() (responder.api method)":[[0,"responder.API.graphql",false]],"group() (responder.api method)":[[0,"responder.API.group",false]],"headers (responder.request property)":[[0,"responder.Request.headers",false]],"headers (responder.response attribute)":[[0,"responder.Response.headers",false]],"is_100() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_100",false]],"is_200() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_200",false]],"is_300() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_300",false]],"is_400() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_400",false]],"is_500() (in module responder.api.status_codes)":[[0,"responder.API.status_codes.is_500",false]],"is_json (responder.request property)":[[0,"responder.Request.is_json",false]],"media (responder.response attribute)":[[0,"responder.Response.media",false]],"media() (responder.request method)":[[0,"responder.Request.media",false]],"method (responder.request property)":[[0,"responder.Request.method",false]],"module":[[0,"module-responder",false]],"mount() (responder.api method)":[[0,"responder.API.mount",false]],"on_event() (responder.api method)":[[0,"responder.API.on_event",false]],"params (responder.request property)":[[0,"responder.Request.params",false]],"path_matches_route() (responder.api method)":[[0,"responder.API.path_matches_route",false]],"path_params (responder.request property)":[[0,"responder.Request.path_params",false]],"redirect() (responder.api method)":[[0,"responder.API.redirect",false]],"request (class in responder)":[[0,"responder.Request",false]],"requests (responder.api property)":[[0,"responder.API.requests",false]],"responder":[[0,"module-responder",false]],"response (class in responder)":[[0,"responder.Response",false]],"route() (responder.api method)":[[0,"responder.API.route",false]],"schema() (responder.api method)":[[0,"responder.API.schema",false]],"serve() (responder.api method)":[[0,"responder.API.serve",false]],"session (responder.request property)":[[0,"responder.Request.session",false]],"session (responder.response attribute)":[[0,"responder.Response.session",false]],"session() (responder.api method)":[[0,"responder.API.session",false]],"sse() (responder.response method)":[[0,"responder.Response.sse",false]],"state (responder.request property)":[[0,"responder.Request.state",false]],"status_code (responder.response attribute)":[[0,"responder.Response.status_code",false]],"stream_file() (responder.response method)":[[0,"responder.Response.stream_file",false]],"template() (responder.api method)":[[0,"responder.API.template",false]],"template_string() (responder.api method)":[[0,"responder.API.template_string",false]],"text (responder.request property)":[[0,"responder.Request.text",false]],"url (responder.request property)":[[0,"responder.Request.url",false]],"url_for() (responder.api method)":[[0,"responder.API.url_for",false]]},"objects":{"":[[0,0,0,"-","responder"]],"responder":[[0,1,1,"","API"],[0,1,1,"","Request"],[0,1,1,"","Response"]],"responder.API":[[0,2,1,"","add_event_handler"],[0,2,1,"","add_route"],[0,2,1,"","after_request"],[0,2,1,"","exception_handler"],[0,2,1,"","graphql"],[0,2,1,"","group"],[0,2,1,"","mount"],[0,2,1,"","on_event"],[0,2,1,"","path_matches_route"],[0,2,1,"","redirect"],[0,3,1,"","requests"],[0,2,1,"","route"],[0,2,1,"","schema"],[0,2,1,"","serve"],[0,2,1,"","session"],[0,2,1,"","template"],[0,2,1,"","template_string"],[0,2,1,"","url_for"]],"responder.API.status_codes":[[0,4,1,"","is_100"],[0,4,1,"","is_200"],[0,4,1,"","is_300"],[0,4,1,"","is_400"],[0,4,1,"","is_500"]],"responder.Request":[[0,2,1,"","accepts"],[0,3,1,"","apparent_encoding"],[0,3,1,"","client"],[0,3,1,"","content"],[0,3,1,"","cookies"],[0,3,1,"","encoding"],[0,3,1,"","full_url"],[0,3,1,"","headers"],[0,3,1,"","is_json"],[0,2,1,"","media"],[0,3,1,"","method"],[0,3,1,"","params"],[0,3,1,"","path_params"],[0,3,1,"","session"],[0,3,1,"","state"],[0,3,1,"","text"],[0,3,1,"","url"]],"responder.Response":[[0,5,1,"","content"],[0,5,1,"","cookies"],[0,2,1,"","file"],[0,5,1,"","formats"],[0,5,1,"","headers"],[0,5,1,"","media"],[0,5,1,"","session"],[0,2,1,"","sse"],[0,5,1,"","status_code"],[0,2,1,"","stream_file"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"],"3":["py","property","Python property"],"4":["py","function","Python function"],"5":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:method","3":"py:property","4":"py:function","5":"py:attribute"},"terms":{"":[0,2,3,4,5,6,8,9],"0":[3,4,6,9],"1":[0,3,4,6,9],"10":[0,6,9],"100":9,"127":[3,4],"13":[3,4],"150mb":4,"16":3,"2":[4,6,8,9],"200":[3,6,8,9],"201":[6,8],"2024":3,"25":3,"26":3,"3":[4,5,6,8,9],"301":[0,6],"308":2,"3600":9,"4":4,"40":2,"400":[0,8,9],"401":9,"404":6,"41d4":6,"422":[8,9],"429":9,"446655440000":6,"500":[2,6,8,9],"5000":9,"5042":[3,4,6],"55":3,"550e8400":6,"60":9,"600":[0,9],"8":[0,3],"80":4,"8000":[4,6],"8192":0,"9":[5,8],"99":8,"A":[0,3,4,5,6,9],"And":6,"As":9,"But":[6,8,9],"By":[3,6,8,9],"For":[3,4,6,8,9],"If":[0,3,5,6,8,9],"In":[3,6,9],"It":[3,5,6,8,9],"No":[8,9],"Not":6,"On":9,"One":[5,6,8],"That":[4,5,6,8],"The":[0,2,3,4,6,8,9],"There":[8,9],"These":6,"To":3,"Will":0,"With":0,"__main__":[4,5,8],"__name__":[4,5,8,9],"_route_for":2,"a2wsgi":2,"a716":6,"abc123":[8,9],"abf":3,"abil":2,"abl":0,"about":[4,6,8],"abov":[3,5],"accept":[0,2,3,6,8,9],"access":[3,4,9],"acm":3,"acquir":[3,7],"across":[4,9],"activ":7,"actual":[0,6,9],"ad":[1,9],"adapt":[3,9],"add":[0,1,2,6],"add_event_handl":0,"add_head":9,"add_request_id":0,"add_rout":[0,9],"add_tim":[8,9],"add_vers":8,"addit":0,"address":[0,3,6,9],"adher":2,"affect":9,"after":[0,5,6],"after_request":[0,1,8,9],"ag":9,"against":[0,4,9],"agre":6,"ai":9,"alias":2,"alic":9,"all":[0,2,3,4,6,7,8,9],"alloc":8,"allow":9,"allow_credenti":[0,9],"allow_head":[0,9],"allow_method":[0,9],"allow_origin":[0,9],"allow_origin_regex":0,"allowed_host":[0,9],"alongsid":9,"alpin":4,"alreadi":[0,8,9],"also":[0,2,3,6,8,9],"altern":[0,9],"alwai":[4,5,6,9],"an":[0,3,6,9],"angular":9,"ani":[0,4,5,6,8,9],"annot":[5,6,9],"annual":[6,9],"answer":9,"anyon":5,"anyth":6,"anywher":[4,9],"api":[2,3,4,5,6,9],"apispec":2,"app":[0,2,3,4,5,8],"appar":0,"apparent_encod":0,"appear":[5,9],"append":3,"applic":[0,2,4,5,6,8,9],"approach":9,"appropri":9,"approx":3,"ar":[2,3,4,5,6,8,9],"architectur":9,"aren":6,"arg":0,"argument":[0,2,6],"around":[0,5],"arriv":6,"arrow":3,"asgi":[0,2,4,5,6,9],"ask":[5,9],"assert":8,"asset":[3,4,9],"assum":3,"async":[0,1,2,4,5,6,8,9],"asynccontextmanag":9,"asynchron":[2,4],"attach":9,"attack":[4,9],"attr":3,"attribut":[0,3],"auth":[5,9],"auth_check":9,"authent":[6,9],"author":[3,6,7,9],"auto":9,"auto_escap":0,"autobuild":7,"automat":[0,4,5,6,8,9],"avail":[3,6],"avoid":[8,9],"aw":[3,4],"await":[0,6,8,9],"az":3,"azur":[3,4],"b":[6,8],"back":[0,5,6,9],"background":[2,5],"backlog":5,"bad":[8,9],"balanc":4,"bandwidth":9,"bar":9,"base":[0,2,3,5],"base_url":0,"basemodel":[0,8,9],"basic":[3,6],"batteri":5,"battl":5,"becaus":6,"becom":0,"been":3,"befor":[5,6],"before_request":[0,1,2,8,9],"beforehand":3,"behind":[4,9],"being":0,"best":5,"better":5,"bidirect":9,"bin":7,"binari":[6,9],"bind":[0,4,6],"blob":3,"block":[6,8,9],"bodi":[0,6,8,9],"boilerpl":5,"both":[5,6,9],"break":8,"bring":5,"broke":[6,8],"broker":6,"browser":[3,6,7,9],"bucket":[3,9],"buffer":6,"bufix":2,"bug":[2,8],"bugfix":2,"build":[2,4,5,6,7],"built":[2,4,5,6,8,9],"bump":2,"bypass":4,"byte":[0,5,6],"cach":[6,8,9],"caddi":4,"call":[6,8,9],"callabl":0,"came":6,"can":[0,3,4,5,6,8,9],"capac":3,"carri":6,"case":[0,5,6],"cat":9,"catch":9,"cd":7,"ceas":2,"celeri":6,"central":6,"certain":0,"certif":4,"chanc":3,"chang":3,"changelog":5,"channel":9,"chardet":0,"chat":9,"check":[7,8,9],"check_exist":[0,9],"choic":[6,9],"choos":[3,9],"chunk":[0,9],"chunk_siz":0,"ci":2,"circuit":[1,5,9],"class":[2,5,6,8],"clean":[5,9],"cleaner":9,"cleanup":9,"cli":[2,5],"click":5,"client":[0,5,6,8,9],"clone":7,"close":[8,9],"close_database_connection_pool":0,"cloud":[3,5,6],"cmd":4,"code":[0,2,4,6,7,8,9],"collabor":9,"com":[3,6,7,9],"come":[6,9],"command":[3,4],"common":[3,4,6,9],"commonli":6,"commun":9,"compact":9,"compat":3,"complement":[1,9],"complet":9,"compress":[5,6],"comput":6,"concept":[2,9],"concern":6,"conclud":3,"concurr":4,"condit":[8,9],"configur":[2,4,6,8,9],"conflict":2,"confus":8,"connect":[0,4,8,9],"consid":[1,6],"consist":[6,9],"consol":9,"const":9,"consum":9,"contact":0,"contain":[0,4,6],"container":4,"content":[0,2,3,5,6,8,9],"content_typ":[0,8,9],"context":[5,8,9],"contextlib":9,"continu":6,"contract":8,"control":[4,6,9],"convent":4,"convert":[2,6],"convertor":5,"cooki":[0,2,5,6],"copi":4,"cor":[0,2,5,8],"core":[2,4],"coroutin":0,"correctli":4,"correspond":3,"cors_param":[0,9],"count":2,"cpu":[4,6],"creat":[0,5,7,8,9],"create_item":[0,9],"create_pet":9,"cross":9,"css":9,"csv":9,"curl":[3,6],"current":3,"custom":[0,5,6,8],"cycl":6,"d":8,"dashboard":9,"dask":3,"data":[0,2,6,8,9],"databas":[6,8,9],"date":3,"debug":[0,2,9],"decim":6,"declar":[2,6],"decod":[2,9],"decor":[0,6,9],"deep":5,"def":[0,5,6,8,9],"default":[0,1,2,3,6,8,9],"default_valu":[0,9],"defend":9,"defin":[3,6,9],"definit":0,"delet":[6,9],"deliber":6,"denpend":2,"depend":[2,4,8],"deploi":[4,5],"deploy":5,"depth":9,"describ":9,"descript":[0,9],"design":[3,6,9],"detail":[4,9],"detect":[0,5,9],"dev":2,"develop":[4,9],"devic":9,"dict":[0,5,6,9],"dictionari":0,"differ":[3,8,9],"digit":6,"direct":[2,9],"directli":[5,6,8],"directori":[0,3,6,9],"disabl":8,"disk":[0,6,9],"dispatch":[2,9],"distribut":[3,4,9],"django":[5,9],"dna":5,"do":[3,4,6,9],"doc":[7,8,9],"docker":5,"dockerfil":4,"docs_rout":[0,9],"docstr":9,"document":[2,4,5,7],"doesn":[0,9],"domain":9,"don":[3,6,8,9],"done":4,"down":9,"download":9,"dramat":2,"drop":2,"dropbox":3,"dump":2,"duplex":9,"duplic":9,"dure":[4,8],"dynam":6,"e":0,"e29b":6,"each":[0,4,8,9],"easi":6,"echo":3,"ecosystem":[5,6],"edit":[3,7],"editor":9,"effect":8,"effici":4,"either":[0,3],"element":[0,9],"els":[6,9],"email":6,"emb":9,"enabl":[2,9],"enable_async":6,"enable_hst":[0,9],"encod":[0,3],"end":6,"endpoint":[0,2,8,9],"enforc":9,"engin":6,"enough":4,"ensur":9,"enter":8,"entir":[0,9],"entri":3,"entrypoint":5,"enumer":3,"env":4,"environ":[0,4,6],"eol":2,"error":[0,5],"escap":0,"essenti":9,"etc":[6,8,9],"even":[3,4,9],"event":[0,2,5,6],"event_typ":0,"eventsourc":9,"everi":[0,4,5,6,8,9],"everyth":[4,9],"exactli":[8,9],"exampl":[3,6,9],"exc":[0,8,9],"exceed":9,"except":[0,2,5,8,9],"exception_cl":0,"exception_handl":[0,8,9],"exist":[0,2,3,5],"exit":[8,9],"expect":[3,6],"expir":9,"explain":9,"explicit":[6,9],"explicitli":9,"explor":[1,5,9],"export":9,"expos":4,"expose_head":0,"express":[5,6],"ext":[2,9],"extens":[2,6],"extern":6,"extra":[2,3,4,9],"extract":0,"extrem":6,"f":[0,5,6,8,9],"fail":[8,9],"failur":8,"falcon":[5,9],"fall":0,"fals":[0,8,9],"familiar":5,"fast":[4,5,6,8],"fastapi":5,"faster":9,"favourit":3,"featur":[0,5,6],"feed":9,"feel":5,"few":3,"fido":9,"field":[0,8,9],"figur":[5,6],"file":[0,2,4,5,6],"filenam":[0,8],"filepath":6,"filesystem":[2,3],"fill":6,"filter":[5,6],"find":9,"fine":[4,9],"fire":8,"first":9,"fit":5,"fix":7,"fixtur":5,"flag":[4,9],"flask":[5,9],"flask_app":9,"float":[0,6,8],"fly":4,"follow":6,"font":9,"forg":9,"form":[0,3,6],"format":[0,2,6,7,9],"formerli":9,"forward":[4,5,9],"found":[2,6],"foundat":9,"framework":[5,9],"franca":6,"fresh":8,"from":[0,2,3,4,5,6,8,9],"front":4,"frontend":9,"fsspec":3,"full":[0,2,3,6,9],"full_url":[0,6,9],"fulli":6,"fun":5,"func":0,"function":[5,6,9],"fuse":3,"g":[0,3],"game":9,"gatewai":4,"gc":3,"gener":[0,5,8,9],"get":[0,6,9],"get_us":[0,9],"git":[3,7],"github":[3,7],"give":[3,8,9],"given":[0,8],"gmt":3,"go":6,"goe":9,"good":[4,5,9],"googl":[3,4],"grace":4,"grade":6,"gradual":9,"graphen":[0,2,5,9],"graphiql":[2,5,9],"graphql":[0,2,5],"great":[5,9],"greet":[5,9],"greet_world":5,"greetingresourc":9,"group":[0,5],"grow":9,"guard":5,"guid":6,"guido":6,"gzip":[5,6],"h1":6,"ha":[6,8],"hadoop":3,"half":6,"halv":9,"hand":[0,3],"handl":[4,5,6],"handle_value_error":[0,9],"handler":[0,3,5,8,9],"happen":[5,6],"hard":8,"have":[0,2,3,6,8],"hdf":3,"head":3,"header":[0,4,5,6,9],"heavi":6,"hello":[0,3,5,8,9],"hello_html":6,"hello_json":6,"hello_to":6,"hello_world":6,"helloworld":3,"help":0,"here":[4,6,9],"high":[4,9],"hold":[5,6],"home":5,"honor":[4,6],"hood":9,"hook":[1,5,6],"host":[0,2,3,4,5,6],"hostnam":9,"hour":9,"how":[6,9],"hst":5,"html":[0,2,5,6,9],"http":[0,2,3,4,5,6,7,8,9],"httpie":3,"httponli":9,"httpsredirectmiddlewar":4,"httpx":[2,8],"i":[0,2,3,4,5,6,8,9],"id":[0,5,6],"identifi":9,"imag":[4,6,9],"immedi":[5,6,9],"immut":2,"implement":[3,8],"import":[0,2,3,5,6,8,9],"importlib":3,"improv":2,"includ":[3,4,5,6,7,8,9],"incom":[0,6],"incredibli":9,"independ":4,"index":[0,9],"individu":9,"industri":9,"info":[0,2,9],"inform":0,"infrastructur":[4,5],"inherit":[3,9],"initi":[0,8],"inject":9,"input":[8,9],"insensit":[0,5,6],"instal":[0,3,4,7,9],"instanc":[0,4,5,6,8],"instant":6,"instead":[3,6,8,9],"int":[0,6,9],"integ":[0,6],"intens":6,"intent":5,"intention":9,"interact":[2,9],"interfac":[4,6],"intern":[8,9],"internet":3,"invalid":[8,9],"invoc":3,"invok":3,"io":4,"iot":9,"ip":[6,9],"is_100":0,"is_200":0,"is_300":0,"is_400":0,"is_500":0,"is_class_bas":2,"is_json":[0,6],"is_secur":6,"isn":[6,9],"issu":2,"item":[0,8,9],"itemin":0,"itemout":0,"its":[0,2,3,4],"j":9,"javascript":[5,9],"jinja2":[0,6],"joi":5,"jpeg":9,"jpg":9,"json":[0,3,5,6,9],"just":[4,8,9],"keep":[2,6,8,9],"kei":[0,8,9],"kennethreitz":[3,7],"keyword":[0,6],"kind":3,"know":6,"known":0,"kwarg":0,"languag":9,"larg":[5,9],"larger":8,"later":[6,8],"launch":[4,5],"layout":3,"lazili":0,"lead":8,"learn":[5,9],"length":3,"less":9,"let":[4,6,9],"level":4,"librari":[8,9],"licens":0,"lifespan":[0,2,5],"lift":6,"lightn":4,"lightweight":6,"like":[3,4,5,6,8,9],"limit":[1,4,5],"line":[3,6],"lingua":6,"list":[0,3,6,8,9],"list_item":9,"list_pet":9,"list_us":[0,9],"list_users_v2":9,"live":9,"ll":[5,6,9],"load":[0,2,3,4,6,9],"local":5,"localhost":6,"locat":[0,3,6],"log":[4,9],"log_level":2,"log_respons":9,"logic":[5,6,9],"login":9,"long":6,"look":[5,6],"loop":6,"lower":0,"lowercas":6,"machin":9,"mai":4,"main":3,"make":[4,6,8,9],"malici":9,"manag":[5,8,9],"mani":[4,6,9],"manual":0,"map":[2,6,9],"mark":[2,9],"marshmallow":[0,2,9],"match":[0,6,9],"matter":9,"max_ag":[0,9],"mean":[4,6],"mechan":9,"media":[0,2,5,6,8,9],"memori":[0,6,9],"messag":[0,6,9],"messagepack":5,"metadata":6,"method":[0,5,6,8],"microservic":9,"middelwar":0,"middlewar":[1,2,6,9],"might":6,"migrat":[2,9],"mime":0,"mimetyp":[6,9],"min":9,"minim":[3,4],"minimum":2,"minor":2,"miss":[8,9],"mix":9,"ml":9,"mode":[0,7],"model":[0,8,9],"modern":[4,9],"modifi":9,"modul":[2,5,6,9],"more":[3,4,5,6,9],"most":[4,6,8,9],"mount":[0,2,5],"move":[2,6,9],"msgpack":9,"much":[6,9],"multipart":[2,6],"multipl":[2,3,4,9],"multiplay":9,"must":[0,6,9],"mutabl":5,"mutat":[0,5,6],"my_templ":6,"myapi":4,"n":6,"name":[0,5,6,8,9],"namespac":2,"natur":8,"need":[3,4,5,6,8,9],"negoti":[0,5,6,8,9],"nervou":6,"network":8,"never":9,"new":[0,6,8,9],"next":3,"nginx":4,"non":[5,6],"none":[0,2],"normal":9,"notabl":2,"notasecret":0,"note":[6,9],"notic":6,"notif":9,"now":[2,6,9],"npm":3,"number":6,"o":6,"object":[0,2,3,5,6,9],"objecttyp":[0,9],"obviou":8,"oci":3,"oct":3,"off":6,"offload":4,"often":[6,9],"ok":[3,6,8],"on_delet":9,"on_ev":[0,8,9],"on_get":[5,9],"on_post":[5,9],"on_put":9,"on_request":[5,9],"on_startup":8,"onc":9,"one":[0,3,5,6,9],"ones":[6,9],"onli":[6,9],"onmessag":9,"open":[2,6,7],"open_database_connection_pool":0,"openapi":[0,2,5],"openapi_rout":0,"openapi_them":0,"oper":[6,9],"opt":9,"option":[0,2,3,4,5,6],"order":3,"organ":[8,9],"origin":9,"other":[0,2,3,5,6],"otherwis":[0,9],"out":[5,6],"outgo":6,"over":[0,5,6,9],"overal":2,"overhead":8,"overrid":[0,9],"overwhelm":9,"own":[4,9],"p":4,"pace":9,"packag":[2,3,4],"page":[2,6,9],"pagin":6,"painless":8,"param":[0,2,5,6],"paramet":[0,5,8,9],"parameter":0,"pars":[0,2,6,8,9],"part":[6,9],"pass":[0,5,6,8],"passion":5,"password":3,"patch":[6,9],"path":[0,2,3,5,6,8,9],"path_matches_rout":0,"path_param":[0,6],"pattern":[4,5,6,9],"pdf":[6,8,9],"per":[4,8,9],"perfect":9,"period":9,"perman":6,"permit":9,"persist":9,"person":5,"pet":[0,9],"petin":9,"petout":9,"petschema":[0,9],"photo":9,"piec":9,"pin":2,"pip":[3,4,5,7],"place":[5,9],"placehold":6,"plain":[3,6,9],"platform":[2,5,6],"pleas":3,"pleasant":5,"plugin":2,"point":[3,4],"polici":9,"pool":[5,6,9],"popular":[6,9],"port":[0,3,4,6,8],"portion":0,"possibl":6,"post":[0,6,8,9],"potenti":2,"power":[5,8,9],"practic":9,"prefer":9,"prefix":[0,9],"present":6,"prevent":9,"price":[0,8],"primari":0,"print":9,"privat":9,"process":[4,5,6,8,9],"process_data":6,"produc":6,"product":[4,5,6,8,9],"profil":9,"program":3,"programmat":9,"progress":9,"project":[2,3,7],"propag":8,"proper":6,"properli":8,"properti":[0,6,8],"protect":4,"proto":4,"protocol":[3,6,8,9],"prototyp":[2,5],"provid":0,"proxi":5,"public":9,"push":[4,5,9],"put":[6,9],"py":[2,3,4,8],"pydant":[0,8,9],"pyproject":2,"pytest":[5,7,8],"python":[0,2,3,4,5,6,9],"q":6,"queri":[0,5,6,9],"queue":6,"quick":5,"r":[6,8],"race":8,"railwai":4,"rais":[2,8,9],"raise_server_except":8,"random":0,"rang":[0,9],"rapidoc":[0,9],"rare":6,"rate":[1,4,5],"ratelimit":9,"rather":8,"raw":[3,5,6,9],"re":[0,4,5,6,8,9],"react":[2,9],"read":[0,2,5,9],"readabl":9,"readi":[4,5,6],"real":9,"realli":[8,9],"receiv":[0,6,8,9],"receive_byt":9,"receive_incom":6,"receive_json":9,"receive_text":[8,9],"recommend":9,"reconnect":9,"redirect":[0,2,6],"redoc":[0,9],"reduc":2,"ref":3,"refactor":[2,8],"refer":[3,8],"reflect":3,"refus":9,"regist":[0,8,9],"regular":3,"reject":[8,9],"relat":9,"releas":7,"reliabl":8,"remain":9,"remot":5,"remov":9,"renam":8,"render":[0,1,4,5,9],"render_async":6,"replac":[2,6,9],"report":[6,8,9],"repres":0,"represent":0,"req":[0,5,6,8,9],"request":[2,3,4,5],"request_id":[0,9],"request_model":[0,8,9],"requir":[2,8,9],"research":5,"resolv":[2,9],"resolve_hello":[0,9],"resourc":[6,9],"resp":[0,2,5,6,8,9],"respond":[0,2,4,6,7,8,9],"respons":[3,5,8,9],"response_model":[0,9],"rest":[4,5,9],"restrict":9,"result":9,"resum":2,"resume_incomplet":2,"retri":[0,9],"retriev":9,"return":[0,2,5,6,8,9],"revers":5,"rfc3986":2,"right":[5,6,9],"rout":[0,2,5,8],"router":2,"ruff":7,"run":[0,2,3,5,7,8,9],"runner":4,"s3":3,"safe":2,"sai":6,"said":4,"same":[4,5,6,9],"sandbox":5,"sat":3,"scale":5,"schema":[0,2,5,9],"scope":[0,9],"scratch":5,"search":[0,2,6,9],"second":3,"secret":9,"secret_kei":[0,9],"section":[3,9],"secur":9,"see":[3,4,6,8,9],"select":[0,2,3],"self":[0,9],"semant":2,"send":[0,2,5,8,9],"send_byt":9,"send_json":9,"send_text":[8,9],"sent":[0,5,6],"separ":[6,8],"sequenti":2,"serial":[6,9],"serializ":6,"serv":[0,2,4,5,6,8],"server":[0,2,3,4,5,8],"servestat":2,"servic":[3,5,8,9],"session":[0,2,5,6,8],"session_id":9,"set":[0,2,4,5,6,7,8,9],"set_cooki":[2,9],"set_text":0,"setup":[2,8],"sever":9,"sftp":3,"share":[0,5,6,8,9],"short":[1,5,9],"should":5,"shouldn":0,"show":[2,9],"shut":9,"shutdown":[0,4,5,8,9],"shutdwown":2,"side":[6,9],"sign":[5,6,9],"signatur":[6,9],"simpl":[8,9],"simplecooki":0,"simpler":[5,6,9],"simplest":6,"simul":6,"sinc":[6,8],"singl":[2,3,5,6,9],"size":0,"skip":9,"slash":6,"sleep":[6,8],"slim":4,"slow":6,"slowlori":4,"slug":6,"small":[4,5,8,9],"smaller":4,"smb":3,"so":[3,8,9],"solv":9,"some":[4,9],"someon":[6,9],"someth":[5,6,8,9],"sometim":[6,9],"somewher":6,"sourc":[0,7,9],"spawn":4,"spec":[6,9],"special":9,"specif":[0,3,8,9],"specifi":3,"sphinx":7,"spin":6,"sse":[0,5],"ssh":3,"ssl":4,"stabil":2,"standard":[4,5,9],"starlett":[0,2,5,8],"start":[4,5,9],"startup":[0,2,5,8,9],"state":[0,2,8,9],"statement":5,"static":[0,2,4,5,6],"static_dir":[0,9],"static_rout":[0,2,9],"statu":[0,2,6,8,9],"status_cod":[0,5,6,8,9],"stdlib":2,"still":9,"stop":9,"storag":3,"store":[0,3,9],"str":[0,6,8,9],"straightforward":9,"stranger":[0,9],"stream":[0,2,9],"stream_fil":[0,9],"strict":9,"string":[0,5,6,9],"strip":9,"structur":9,"style":9,"stylesheet":9,"subcommand":3,"subrout":[5,9],"subsequ":9,"subtl":2,"success":6,"successor":4,"suit":8,"suppli":2,"support":[0,1,2,3,4,5,6],"surpris":3,"suspect":5,"swagger":[5,9],"swagger_ui":[0,9],"switch":2,"symbol":3,"sync":[2,5,6],"syntax":[5,6],"synthet":3,"system":[3,6,9],"t":[0,3,4,6,8,9],"take":[5,6],"tamper":9,"target":[2,3],"task":[2,5],"tear":9,"tell":[6,9],"templat":[0,1,2,5],"template_str":[0,6],"templates_dir":0,"termin":4,"terms_of_servic":0,"test":[0,2,5,7],"test_500":[2,8],"test_api":8,"test_create_item":8,"test_custom_error":8,"test_head":8,"test_hello":8,"test_hook":8,"test_json":8,"test_upload":8,"test_valid":8,"test_websocket":8,"test_with_lifespan":8,"testclient":[0,2,8],"text":[0,3,5,6,8,9],"than":[3,8,9],"thei":[6,9],"them":[5,6,8,9],"theme":[0,9],"themselv":9,"thi":[0,2,3,4,6,8,9],"thing":[6,8],"think":6,"those":3,"though":4,"thousand":4,"thread":[5,6],"threadpoolexecutor":2,"three":[6,9],"through":[6,9],"throughput":9,"time":[0,3,5,6,8,9],"time_start":0,"timeout":4,"tip":5,"titl":[0,6,9],"tl":4,"togeth":[5,9],"token":[6,9],"toml":2,"too":[4,8,9],"tool":7,"toolbelt":2,"total":9,"tour":5,"trace":9,"traceback":[2,9],"tradit":9,"traffic":4,"transfer":3,"transport":9,"treat":[0,9],"trick":9,"trigger":8,"true":[0,6,8,9],"trust":5,"trustedhostmiddlewar":4,"tupl":[0,6,8],"two":[6,9],"type":[0,2,3,5,9],"typic":[3,6,9],"typo":[2,9],"ui":[2,5,9],"unauthor":9,"under":[6,9],"understand":6,"unicod":0,"uniqu":9,"unknown":0,"unless":9,"unlik":9,"unmaintain":2,"unmatch":9,"unpin":2,"unrecogn":9,"until":6,"up":[0,2,6,7,9],"updat":[2,6,9],"upgrad":[2,7,9],"upload":[2,5,6],"url":[0,2,3,5,6,8,9],"url_for":[0,8],"urllib":2,"us":[0,2,3,4,5,6,9],"usag":[0,2],"user":[0,3,6,9],"user_id":[6,9],"usernam":9,"usual":[5,8],"utf":[0,3],"util":5,"uuid":[0,6,9],"uuid4":0,"uv":[3,5,7],"uvicorn":[0,2,3,5,6],"uvloop":4,"v1":[0,9],"v2":9,"valid":[3,5,6,9],"valu":[0,5,6,8,9],"valueerror":[0,8,9],"variabl":[0,4,6],"ve":[5,8],"venv":7,"verb":9,"veri":[0,6],"verifi":8,"version":[0,2,3,8,9],"via":[2,9],"view":[0,2,5,6,8],"virtual":4,"virtualenv":7,"visit":9,"vue":9,"w":[8,9],"wa":[0,2,6],"wai":[4,6,9],"walk":[6,9],"want":[0,4,5,6,8,9],"watch":7,"web":[3,4,5,9],"webdav":3,"websit":9,"websocket":[0,1,2,4,5],"websocket_connect":8,"well":5,"went":5,"wget":3,"what":[6,8,9],"whatev":6,"when":[2,3,4,5,6,8,9],"where":[3,4,6,9],"whether":[0,6],"which":[2,3,6,8,9],"whichev":5,"while":[6,9],"whitenois":2,"who":[5,6],"widget":8,"wildcard":9,"window":2,"wire":6,"within":[2,3],"without":[0,4,6,8,9],"won":[6,8,9],"work":[3,4,5,6,9],"workdir":4,"worker":4,"workgroup":3,"world":[0,3,5,8,9],"worri":8,"would":[3,8],"wrap":[6,8,9],"write":[6,9],"wrong":9,"wsgi":[0,2,4,5,9],"x":[0,4,6,8,9],"x89png":6,"xml":0,"xss":9,"yaml":[0,2,5,6,8,9],"yield":[0,9],"yml":[0,9],"you":[0,3,4,6,8,9],"your":[3,4,6,8,9],"yourself":6,"zero":4},"titles":["API Documentation","Backlog","Changelog","Responder CLI","Deployment","Responder","Quick Start","Development Sandbox","Testing","Feature Tour"],"titleterms":{"0":2,"01":2,"02":2,"03":2,"04":2,"08":2,"09":2,"1":2,"10":2,"11":2,"12":2,"13":2,"15":2,"16":2,"17":2,"18":2,"19":2,"2":2,"20":2,"2018":2,"2019":2,"2026":2,"22":2,"23":2,"24":2,"25":2,"26":2,"27":2,"28":2,"29":2,"3":2,"4":2,"5":2,"6":2,"7":2,"8":2,"9":2,"The":5,"ad":2,"after":[8,9],"api":[0,8],"app":9,"applic":3,"background":6,"backlog":1,"base":9,"befor":[8,9],"build":3,"chang":2,"changelog":2,"class":[0,9],"cli":3,"cloud":4,"convertor":6,"cooki":[8,9],"cor":9,"creat":6,"custom":9,"deploy":4,"deprec":2,"develop":7,"directli":4,"docker":4,"document":[0,9],"entrypoint":3,"error":[8,9],"event":[8,9],"featur":9,"file":[3,8,9],"filter":9,"fix":2,"fixtur":8,"function":0,"futur":1,"get":[5,8],"graphql":9,"group":9,"guid":5,"handl":[8,9],"header":8,"hello":6,"hook":[8,9],"host":9,"hst":9,"id":9,"idea":[1,5],"instal":5,"instanc":3,"javascript":3,"json":8,"launch":3,"lifespan":[8,9],"limit":9,"local":[3,4],"messagepack":9,"method":9,"modul":3,"mount":9,"name":3,"non":3,"openapi":9,"oper":7,"other":9,"paramet":6,"platform":4,"project":5,"proxi":4,"quick":6,"rate":9,"read":6,"remot":3,"remov":2,"render":6,"request":[0,6,8,9],"respond":[3,5],"respons":[0,6],"revers":4,"rout":[6,9],"run":[4,6],"sandbox":7,"send":6,"sent":9,"serv":9,"server":[6,9],"servic":[0,6],"session":9,"setup":7,"sse":9,"standard":3,"start":[6,8],"static":9,"support":9,"task":6,"templat":6,"test":8,"tip":8,"tour":9,"trust":9,"type":6,"unreleas":2,"upload":8,"us":8,"user":5,"util":0,"uvicorn":4,"v0":2,"v1":2,"v2":2,"v3":2,"valid":8,"view":9,"web":[0,6],"websocket":[8,9],"what":5,"world":6,"you":5}}) \ No newline at end of file diff --git a/testing.html b/testing.html index f146d38..af5c29c 100644 --- a/testing.html +++ b/testing.html @@ -303,7 +303,7 @@ network overhead. Avoid API() configuration (like cors=True), create a new instance in the test rather than sharing the fixture.

    -
  • Use ``api.url_for()`` instead of hard-coded paths. It’s a small +

  • Use api.url_for() instead of hard-coded paths. It’s a small thing, but it makes refactoring painless.

  • Test the contract, not the implementation. Assert on status codes, response bodies, and headers — not on internal state.

  • diff --git a/tour.html b/tour.html index 920ae01..37d5e51 100644 --- a/tour.html +++ b/tour.html @@ -42,12 +42,24 @@

    Feature Tour

    -

    This section walks through Responder’s features in detail. Each section -includes working code examples you can copy into your application.

    +

    This section walks through Responder’s features in depth. Each section +explains the concept, shows working code, and explains the design choices +behind it. If you’re new to web development, this is a good place to learn +how modern web frameworks work under the hood.

    Method Filtering

    -

    By default, a route matches all HTTP methods. If you want to restrict a -route to specific methods, pass the methods parameter:

    +

    HTTP defines several methods (also called verbs) that describe what a +client wants to do with a resource. The most common are:

    +
      +
    • GET — retrieve data

    • +
    • POST — create something new

    • +
    • PUT — replace something entirely

    • +
    • PATCH — update part of something

    • +
    • DELETE — remove something

    • +
    +

    By default, a Responder route matches all methods. This is fine for simple +endpoints, but REST APIs typically map different methods to different +operations. Use the methods parameter to restrict a route:

    @api.route("/items", methods=["GET"])
     def list_items(req, resp):
         resp.media = {"items": []}
    @@ -58,13 +70,19 @@ route to specific methods, pass the <
         resp.media = {"created": data}
     
    -

    Note the check_existing=False — this allows you to register multiple -handlers for the same path with different methods.

    +

    Note the check_existing=False — Responder normally prevents you from +registering two routes with the same path (to catch typos). When you +intentionally want multiple handlers for the same path with different +methods, you need to opt in.

    Class-Based Views

    -

    For more complex resources, you can use class-based views. Responder will -dispatch to the appropriate method handler based on the HTTP method:

    +

    Function-based views are great for simple endpoints, but sometimes you want +to group related HTTP methods together into a single resource. This is +where class-based views come in — a pattern popularized by +Falcon.

    +

    Responder dispatches to the appropriate method handler based on the HTTP +method:

    @api.route("/{greeting}")
     class GreetingResource:
         def on_get(self, req, resp, *, greeting):
    @@ -81,13 +99,17 @@ dispatch to the appropriate method handler based on the HTTP method:

    The on_request method is called for all HTTP methods, much like middleware scoped to a single route. Method-specific handlers (on_get, on_post, on_put, on_delete, etc.) are called after.

    -

    No inheritance required — just define a class with the right method names.

    +

    No inheritance required — just define a class with the right method names. +This is simpler than Django’s View classes and more Pythonic than +framework-specific base classes.

    Lifespan Events

    -

    Modern applications often need to set up resources on startup (database -connections, caches, ML models) and tear them down on shutdown. Responder -supports the lifespan context manager pattern:

    +

    Real applications need to set up resources when they start (database +connection pools, ML models, caches) and tear them down when they stop. +This is called the application lifespan.

    +

    The modern approach is the context manager pattern, where startup and +shutdown are two halves of the same block:

    from contextlib import asynccontextmanager
     
     @asynccontextmanager
    @@ -101,7 +123,10 @@ supports the lifespan context manager pattern:

    api = responder.API(lifespan=lifespan)
    -

    You can also use the traditional event decorator style:

    +

    Everything before yield runs at startup. Everything after runs at +shutdown. If startup fails, the server won’t start. If shutdown raises, +it’s logged but the server still exits.

    +

    The traditional event decorator style also works:

    @api.on_event("startup")
     async def startup():
         print("starting up")
    @@ -111,52 +136,65 @@ supports the lifespan context manager pattern:

    print("shutting down")
    -

    The context manager approach is preferred for new code — it makes the -startup/shutdown relationship explicit and keeps related code together.

    +

    The context manager is preferred for new code — it keeps related startup +and shutdown logic together and makes resource cleanup more explicit.

    Serving Files

    -

    Serve files from disk with automatic content-type detection. Responder -uses Python’s mimetypes module to figure out the right Content-Type -header for you:

    +

    Web applications often need to serve files — downloads, reports, images. +Responder makes this simple with resp.file(), which reads a file from +disk and sets the Content-Type header automatically using Python’s +mimetypes module:

    @api.route("/download")
     def download(req, resp):
         resp.file("reports/annual.pdf")
     
    -

    You can override the content type if needed:

    +

    You can override the content type if the automatic detection isn’t right:

    @api.route("/image")
     def image(req, resp):
         resp.file("photos/cat.jpg", content_type="image/jpeg")
     
    +

    For large files, use resp.stream_file() to avoid loading the entire +file into memory. This streams the file in chunks:

    +
    @api.route("/export")
    +def export(req, resp):
    +    resp.stream_file("data/export.csv")
    +
    +

    Custom Error Handling

    -

    By default, unhandled exceptions result in a 500 Internal Server Error. -You can register custom handlers for specific exception types to return -structured error responses:

    +

    In production, you don’t want your users to see raw Python tracebacks. +Responder lets you register custom handlers for specific exception types, +so you can return clean, structured error responses:

    @api.exception_handler(ValueError)
     async def handle_value_error(req, resp, exc):
         resp.status_code = 400
         resp.media = {"error": str(exc)}
     
    -

    Now, any route that raises a ValueError will return a clean 400 response -with a JSON error message instead of a generic 500 page.

    +

    Now, any route that raises a ValueError will return a clean JSON +response with a 400 status code instead of a generic 500 error page.

    +

    This is a common pattern in API development — you define your own exception +classes for different error conditions, register handlers for each, and +your API always returns consistent, machine-readable error responses.

    Before-Request Hooks

    -

    Run code before every request. This is useful for logging, adding common -headers, or setting up per-request state:

    +

    Sometimes you need to run the same code before every request — +authentication checks, request logging, adding common headers, or setting +up per-request state. Before-request hooks let you do this without +duplicating code in every route:

    @api.route(before_request=True)
     def add_headers(req, resp):
    -    resp.headers["X-API-Version"] = "3.1"
    +    resp.headers["X-API-Version"] = "3.2"
     
    -

    Short-circuiting: If your hook sets resp.status_code, the route -handler will be skipped entirely and the response will be sent immediately. -This is the pattern for authentication guards:

    +

    Short-circuiting is the really powerful part. If your hook sets +resp.status_code, the route handler is skipped entirely and the +response is sent immediately. This is the pattern for authentication:

    @api.route(before_request=True)
     def auth_check(req, resp):
         if "Authorization" not in req.headers:
    @@ -165,17 +203,32 @@ This is the pattern for authentication guards:

    If the Authorization header is missing, the client gets a 401 response -and the actual route handler never runs.

    -

    WebSocket hooks work the same way:

    -
    @api.before_request(websocket=True)
    -async def ws_auth(ws):
    -    await ws.accept()
    +and the actual route handler never runs. This is cleaner than adding
    +auth checks to every individual route.

    +
    +
    +

    After-Request Hooks

    +

    The complement to before-request hooks. After-request hooks run after the +route handler completes but before the response is sent. They’re useful +for logging, adding response headers, or any post-processing:

    +
    @api.after_request()
    +def log_response(req, resp):
    +    print(f"{req.method} {req.full_url} -> {resp.status_code}")
    +
    +@api.after_request()
    +async def add_timing(req, resp):
    +    resp.headers["X-Served-By"] = "responder"
     

    WebSocket Support

    -

    Responder supports WebSockets for real-time, bidirectional communication:

    +

    HTTP is a request-response protocol — the client asks, the server answers. +But some applications need real-time, bidirectional communication: chat +apps, live dashboards, multiplayer games, collaborative editors.

    +

    WebSockets solve this by +upgrading an HTTP connection into a persistent, full-duplex channel where +both sides can send messages at any time:

    @api.route("/ws", websocket=True)
     async def websocket(ws):
         await ws.accept()
    @@ -187,13 +240,49 @@ and the actual route handler never runs.

    You can send and receive in multiple formats:

      -
    • send_text / receive_text — plain text

    • -
    • send_json / receive_json — JSON objects

    • +
    • send_text / receive_text — plain text strings

    • +
    • send_json / receive_json — JSON objects (auto-serialized)

    • send_bytes / receive_bytes — raw binary data

    +

    WebSocket routes are marked with websocket=True in the route decorator. +They receive a ws object instead of req and resp.

    +
    +
    +

    Server-Sent Events (SSE)

    +

    SSE is a simpler alternative to WebSockets for one-way real-time +communication — the server pushes events to the client, but the client +can’t send messages back. This is perfect for live feeds, progress bars, +notification streams, and AI response streaming.

    +

    Unlike WebSockets, SSE works over plain HTTP, is automatically reconnected +by the browser, and doesn’t require any special client-side libraries:

    +
    @api.route("/events")
    +async def events(req, resp):
    +    @resp.sse
    +    async def stream():
    +        for i in range(10):
    +            yield {"data": f"message {i}"}
    +
    +
    +

    On the client side, you consume SSE events with JavaScript’s built-in +EventSource API:

    +
    const source = new EventSource("/events");
    +source.onmessage = (event) => {
    +    console.log(event.data);
    +};
    +
    +
    +

    Each yielded value can be a string (treated as data) or a dict with the +standard SSE fields:

    +
    yield {"event": "update", "data": "hello", "id": "1", "retry": "5000"}
    +yield "simple string message"
    +
    +

    GraphQL

    +

    GraphQL is a query language for APIs that lets +clients request exactly the data they need — no more, no less. Instead of +multiple REST endpoints, you define a schema and let clients query it.

    Responder includes built-in GraphQL support via Graphene. Set up a full GraphQL endpoint with a single method call:

    @@ -208,16 +297,20 @@ with a single method call:

    api.graphql("/graphql", schema=graphene.Schema(query=Query)) -

    Visiting /graphql in a browser renders the GraphiQL interactive IDE, -where you can explore your schema and test queries. Programmatic clients -can POST JSON queries to the same endpoint.

    +

    Visiting /graphql in a browser renders the +GraphiQL interactive IDE, where +you can explore your schema, write queries, and see results in real-time. +Programmatic clients can POST JSON queries to the same endpoint.

    You can access the Responder request and response objects in your resolvers through info.context["request"] and info.context["response"].

    OpenAPI Documentation

    -

    Responder can generate an OpenAPI schema and serve interactive API -documentation automatically:

    +

    OpenAPI (formerly Swagger) is the industry +standard for describing REST APIs. An OpenAPI specification lets you +auto-generate interactive documentation, client libraries, and validation +logic.

    +

    Responder generates OpenAPI specs from your code:

    api = responder.API(
         title="Pet Store",
         version="1.0",
    @@ -232,9 +325,11 @@ documentation automatically:

  • Interactive Swagger UI documentation at /docs

  • There are three ways to document your endpoints.

    -

    Pydantic models — the recommended approach for new APIs. Use -request_model and response_model to annotate your routes, and -Responder will generate the schema automatically:

    +

    Pydantic models — the recommended approach. Use request_model and +response_model to annotate your routes, and Responder generates the +schema automatically. When request_model is set, request bodies are +also validated automatically — invalid inputs get a 422 response with +detailed error messages:

    from pydantic import BaseModel
     
     class PetIn(BaseModel):
    @@ -253,18 +348,10 @@ Responder will generate the schema automatically:

    resp.media = {"id": 1, **data}
    -

    This generates a full OpenAPI path with requestBody and responses -schemas, all linked by $ref to your Pydantic models in -components/schemas.

    -

    You can also register standalone schemas with the @api.schema decorator:

    -
    @api.schema("Pet")
    -class Pet(BaseModel):
    -    name: str
    -    age: int = 0
    -
    -
    -

    YAML docstrings — inline your OpenAPI spec directly in the docstring. -This gives you full control over every detail:

    +

    When response_model is set, the response is serialized through the +model — extra fields are stripped and types are enforced.

    +

    YAML docstrings — for full control, embed OpenAPI YAML in the +docstring:

    @api.route("/pets")
     def list_pets(req, resp):
         """A list of pets.
    @@ -278,8 +365,7 @@ This gives you full control over every detail:

    resp.media = [{"name": "Fido"}]
    -

    Marshmallow schemas — if you’re already using marshmallow for -validation, Responder integrates with it via the apispec plugin:

    +

    Marshmallow schemas — if you’re already using marshmallow:

    from marshmallow import Schema, fields
     
     @api.schema("Pet")
    @@ -287,16 +373,39 @@ validation, Responder integrates with it via the apispec plugin:

    name = fields.Str()
    -

    All three approaches can be mixed in the same API. Pydantic models, -marshmallow schemas, and YAML docstrings all contribute to the same -generated OpenAPI specification.

    -

    You can choose from multiple documentation themes: -swagger_ui (default), redoc, rapidoc, or elements.

    +

    All three approaches can be mixed in the same API. You can choose from +multiple documentation themes: swagger_ui (default), redoc, +rapidoc, or elements.

    +
    +
    +

    Route Groups

    +

    As your application grows, you’ll want to organize routes logically. +Route groups let you share a URL prefix across related endpoints — a +common pattern for API versioning:

    +
    v1 = api.group("/v1")
    +
    +@v1.route("/users")
    +def list_users(req, resp):
    +    resp.media = []
    +
    +@v1.route("/users/{user_id:int}")
    +def get_user(req, resp, *, user_id):
    +    resp.media = {"id": user_id}
    +
    +v2 = api.group("/v2")
    +
    +@v2.route("/users")
    +def list_users_v2(req, resp):
    +    resp.media = {"users": [], "total": 0}
    +
    +
    +

    This keeps your code organized without affecting the routing logic.

    Mounting Other Apps

    -

    Responder can mount any WSGI or ASGI application at a subroute. This means -you can gradually migrate from Flask, or run multiple frameworks side by side:

    +

    Responder can mount any WSGI or ASGI application at a subroute. This is +incredibly useful for gradual migrations — you can run Flask and Responder +side by side, moving routes over one at a time:

    from flask import Flask
     
     flask_app = Flask(__name__)
    @@ -310,10 +419,14 @@ you can gradually migrate from Flask, or run multiple frameworks side by side:
     

    Requests to /flask/ will be handled by Flask. Everything else goes through Responder. Both WSGI and ASGI apps are supported — Responder -wraps WSGI apps automatically.

    +wraps WSGI apps in an ASGI adapter automatically.

    Cookies

    +

    Cookies are +small pieces of data that the server asks the browser to store and send +back with every subsequent request. They’re the foundation of sessions, +authentication tokens, and user preferences on the web.

    Reading and writing cookies is straightforward:

    # Read cookies from the request
     session_id = req.cookies.get("session_id")
    @@ -322,24 +435,27 @@ wraps WSGI apps automatically.

    resp.cookies["hello"] = "world"
    -

    For more control over cookie directives, use set_cookie:

    +

    For production use, you’ll want to set security directives. The +httponly flag prevents JavaScript from reading the cookie (defending +against XSS attacks), and secure ensures it’s only sent over HTTPS:

    resp.set_cookie(
         "token",
         value="abc123",
    -    max_age=3600,
    -    secure=True,
    -    httponly=True,
    +    max_age=3600,        # expires in 1 hour
    +    secure=True,         # HTTPS only
    +    httponly=True,        # no JavaScript access
         path="/",
     )
     
    -

    Supported directives: key, value, expires, max_age, -domain, path, secure, httponly.

    Static Files

    -

    Static files are served from the static/ directory by default:

    +

    Most web applications serve static assets — CSS stylesheets, JavaScript +files, images, fonts. Responder serves these from the static/ directory +by default:

    api = responder.API(static_dir="static", static_route="/static")
     
    -

    Place your CSS, JavaScript, images, and other assets in the static/ -directory and they’ll be served automatically.

    -

    For single-page applications, you can serve index.html as the default -response for all unmatched routes:

    +

    Place your assets in the static/ directory and they’ll be served +automatically at /static/style.css, /static/app.js, etc.

    +

    For single-page applications (React, Vue, Angular), you can serve +index.html as the default response for all unmatched routes:

    api.add_route("/", static=True)
     
    -

    You can add additional static directories at runtime:

    -
    api.static_app.add_directory("extra_assets")
    -
    -

    CORS

    -

    Enable Cross-Origin Resource Sharing for your API:

    +

    CORS (Cross- +Origin Resource Sharing) is a security mechanism that controls which +websites can make requests to your API. Browsers enforce this — if your +API is at api.example.com and your frontend is at app.example.com, +the browser will block requests unless your API explicitly allows it.

    +

    Enable CORS and configure which origins are allowed:

    api = responder.API(cors=True, cors_params={
    -    "allow_origins": ["https://example.com"],
    +    "allow_origins": ["https://app.example.com"],
         "allow_methods": ["GET", "POST"],
         "allow_headers": ["*"],
         "allow_credentials": True,
    @@ -390,98 +506,54 @@ response for all unmatched routes:

    })
    -

    The default CORS policy is restrictive — you must explicitly enable the -origins, methods, and headers your frontend needs.

    +

    The default policy is restrictive — you must explicitly allow each origin. +Using ["*"] for allow_origins permits any website to call your API, +which is fine for public APIs but not for private ones.

    HSTS

    -

    Force all traffic to HTTPS with a single flag:

    +

    HSTS +(HTTP Strict Transport Security) tells browsers to always use HTTPS when +communicating with your server. Once a browser sees the HSTS header, it +will refuse to connect over plain HTTP, even if the user types http:// +in the address bar:

    api = responder.API(enable_hsts=True)
     
    -

    This adds the Strict-Transport-Security header and redirects HTTP -requests to HTTPS.

    Trusted Hosts

    -

    Protect against HTTP Host header attacks by restricting which hostnames -your application will respond to:

    +

    The Host header in an HTTP request tells the server which domain name +the client used. Attackers can forge this header to trick your application +into generating URLs to malicious domains (a class of attack called Host +header injection).

    +

    Restrict which hostnames your application accepts:

    api = responder.API(allowed_hosts=["example.com", "*.example.com"])
     
    -

    Requests with a Host header that doesn’t match any of the patterns -will receive a 400 Bad Request response. Wildcard domains are supported.

    -

    By default, all hostnames are allowed.

    -
    -
    -

    Server-Sent Events (SSE)

    -

    Stream real-time updates to the client using Server-Sent Events. This is -great for live feeds, progress updates, and AI streaming responses:

    -
    @api.route("/events")
    -async def events(req, resp):
    -    @resp.sse
    -    async def stream():
    -        for i in range(10):
    -            yield {"data": f"message {i}"}
    -
    -
    -

    Each yielded value can be a string (treated as data) or a dict with -data, event, id, and retry fields:

    -
    yield {"event": "update", "data": "hello", "id": "1"}
    -yield "simple string message"
    -
    -
    -
    -
    -

    Streaming Files

    -

    For large files, use resp.stream_file() to stream the content without -loading the entire file into memory:

    -
    @api.route("/download")
    -def download(req, resp):
    -    resp.stream_file("large-dataset.csv")
    -
    -
    -

    For small files where memory isn’t a concern, resp.file() loads the -entire file at once — simpler but less efficient for large files.

    -
    -
    -

    After-Request Hooks

    -

    Run code after every request, useful for logging, adding headers, or -cleanup:

    -
    @api.after_request()
    -def log_response(req, resp):
    -    print(f"{req.method} {req.full_url} -> {resp.status_code}")
    -
    -
    -
    -
    -

    Route Groups

    -

    Organize related routes with a shared URL prefix. Useful for API versioning -and logical grouping:

    -
    v1 = api.group("/v1")
    -
    -@v1.route("/users")
    -def list_users(req, resp):
    -    resp.media = []
    -
    -@v1.route("/users/{user_id:int}")
    -def get_user(req, resp, *, user_id):
    -    resp.media = {"id": user_id}
    -
    -
    +

    Requests with unrecognized hosts get a 400 Bad Request. Wildcard +patterns are supported. By default, all hostnames are allowed.

    Request ID

    -

    Auto-generate unique request IDs for tracing and debugging. If the client -sends an X-Request-ID header, it’s forwarded; otherwise a new UUID is -generated:

    +

    In distributed systems, tracing a single request across multiple services +is essential for debugging. Request IDs are unique identifiers attached to +each request — if something goes wrong, you can search your logs for that +ID and find every related event.

    +

    Responder can auto-generate request IDs. If the client sends an +X-Request-ID header (common in microservice architectures), it’s +forwarded. Otherwise, a new UUID is generated:

    api = responder.API(request_id=True)
     
    +

    The ID appears in the X-Request-ID response header.

    Rate Limiting

    -

    Built-in token bucket rate limiter:

    +

    Rate limiting prevents individual clients from overwhelming your API with +too many requests. It’s essential for public APIs, and good practice even +for internal ones.

    +

    Responder includes a built-in token bucket rate limiter:

    from responder.ext.ratelimit import RateLimiter
     
     limiter = RateLimiter(requests=100, period=60)  # 100 req/min
    @@ -489,19 +561,24 @@ generated:

    When the limit is exceeded, clients receive a 429 Too Many Requests -response with Retry-After and X-RateLimit-Remaining headers.

    +response with a Retry-After header. Every response includes +X-RateLimit-Limit and X-RateLimit-Remaining headers so clients +can pace themselves.

    +

    The rate limiter is per-client, keyed by IP address.

    MessagePack

    -

    In addition to JSON and YAML, Responder supports MessagePack for efficient -binary serialization:

    -
    # Decode MessagePack request body
    +

    MessagePack is a binary serialization format +that’s more compact and faster to parse than JSON. It’s useful for +high-throughput APIs, IoT devices, and anywhere bandwidth matters.

    +

    Responder supports MessagePack alongside JSON and YAML:

    +
    # Decode a MessagePack request body
     data = await req.media("msgpack")
    -
    -# Content negotiation also works — clients can send
    -# Accept: application/x-msgpack to receive MessagePack responses.
     
    +

    Content negotiation works too — clients can send +Accept: application/x-msgpack to receive MessagePack responses +instead of JSON.

    @@ -535,9 +612,12 @@ binary serialization:

  • Serving Files
  • Custom Error Handling
  • Before-Request Hooks
  • +
  • After-Request Hooks
  • WebSocket Support
  • +
  • Server-Sent Events (SSE)
  • GraphQL
  • OpenAPI Documentation
  • +
  • Route Groups
  • Mounting Other Apps
  • Cookies
  • Cookie-Based Sessions
  • @@ -545,10 +625,6 @@ binary serialization:

  • CORS
  • HSTS
  • Trusted Hosts
  • -
  • Server-Sent Events (SSE)
  • -
  • Streaming Files
  • -
  • After-Request Hooks
  • -
  • Route Groups
  • Request ID
  • Rate Limiting
  • MessagePack