kmE^p-}bkDAH=d()pmD4 zoVWe4|NEjj>uS3@An$F~1~Il(TDwCa?`>A2xSmpLcL?OY&1w*pwYqB{H?~=(6vMY$ zihDKF4CufxO}}j1Zaa?oTGr9fl((6tTO7I_$7x&2o4fFuuk%nT+iTAJO#lgQ_?9Ke zw$k2p;1Vf86QWmqn7n2CHI!lsPyH71*@m3No{F1w8y{I?tw@wW!h6vV?%y8BO>LGC zb|1xXy$6b~#jhY;RX~7;-Rt#V1HQf)NQ(yd)I@Fp#OO3BO#vi~lO*}@hqnhJZn&sR zJ!_U_z@IK0`c`2;Km{#UVF^T&%JK!P$FNLGqx*f` zsKFkiDdk;ZJW5WX)z7 z?zU5CO$DT_Y3OMQh>M!>N^b!aEJKLVB9NA_7j6d?kNh4oLS^r_sIw !=K-t$l{x7)>f;!;woOZyhK3sk+8Qb`%N|iKHgXWVv9cf9XKd$z=N$0 zDBq&RiR*zai;sh%rZoeY!i4T&SN0(m4GDG$O*=i6fw)V71)s?|0#pq66wRRJYuGeP zt|RM(l1)Gc d9>mSGQVR zRNAAv`k)@C)9JuX*v&xIdI%NT+Bi<>cr4R&a4-hq6g@0z)QxI|NIgS%Y>ap O#qNfqeMcXOA8|8vo+fKx(C@c;{uOj(?X*(W_9=sDN1Lou!u1 z!QgVjbgu#;XEw}nspu|Q%U%TpVg>FYkGh4aa-?0wP!1OaP7PS58ed2I)|D}ac8zGV zDfL*AWguy?B74Vo1Vj}qE&~CJ)pJXm<%xY kTBKDNK{GMdK }$mLZHK5rQFUEdbGqc}Eeh4OY#@ z8!8|;lO2w?SkX9u>5$2C@kSF@f #_jk zESJmTH{d_+i!)A0GSqh*pG@HNG!@5jp5%K2>_0exm@KC?gh230^mxEP1}C|^q!y(c zTmr(2qp;-dBCCcV$XzI%XqZV0d&DIbj9L3#eG!OmLI>N_OW)vQ>K)JvpppaK=z+TI zK;CZ$PC4OeJn(CbhsH7FQJ<|U9)cBX#&K x z9`iq^&^ReV E7khYV6_+pyydyWNA6FX7*p>ZdBJdhejD6-titJcg;-;Fv2Qv*~$$l1$6<@N7^V zW(0~B(OgIu9!Y
+c65`<1h8-RyV-COZKkXD>3k48$?kkiEyeOGzC3NVee!%8>l*ExFl#e zf`ICup>-p 07Dp(1;}VHg!>NN_60v5@0_lu98-p2F#-}8n%-;A!(c8mt zSK~~gx6}jgV!iufrN&d0Q?#n;Su3rz?;a#a1DyKF?0P^J<^CU*fJ|$F#0=zuP7W9d zFO0MnNUb9@KV6HzxPIksRzLy7?ql5$yMt;U;0B9oL7H}7uLr_allHBESnJ;nTKkH^ zr+S;E1Ty$z`lsPRBM`Jyj2Wy{m=+9xP8Lc7hwkM_sHDF3<61uLTo^%V5xB0$WvgQQ z!KGai-|FJl3C2Q;Kv4abjTo&(ePU0!zk>TMU3e0fTt?80Kzy@MFe(z~0IxP2z_83f zlRyl91q7l6rm99-$Vle3~0$QbeLew z70PrH ()Yw~nQ2_)qd;?JKiY>23UpxXDs@Py(kjIyGKDP)5{hr-^y^kGykqE)GPhZ2bB z@5@=rlhGlacVxS^48#@+AO;qcEiT7)m QmZ?I~-usuwHCqh8Yw &qr?^})#(53}PmNyF@3_DY2J?xg~P-!vUelX;ekFv}hu zpCr#F>p+g _!=QYFKyrDODS0#my%NX+n&JgFDqF}MD^$;D z8Hnc^#u^YT9hX+zk~7w!8f5@C2As#2J_KuLc2%-; tT2ZOT*BF+!nD{ zKS8xbk^~aA$H~Q$qcqB2B=IyELnfVv5CrAtNg5xd`S>(BpRWNam5lPd+z?R$5%Y7( z05jP__vUjhozS)XLdl~(=q-T+rLJhU+vH_U4h9^msxJU3x+#H>Q!ipBn VEI sQ?f+p+$AJPmY5H{{aX}r~FGGVhS;af7wa)om5Q%V_U zLC&2gS@Lx<21EWLIXz0I$$1901p?qSIZgIXlIR~vw6hKblzbRt5#{nQtd9h4DGk*h zH4u}u9 TDn`Xk=3m!aArsTG-wOwYwQ763%VX)K~_ zZysVP{VY#kjUT^=C(&20&eCV=VegxehM@Od6!z LHGxglzf5TPF^?v2|F3HV) Px#AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUz9Z*bEMH>+jMnXlz(&1%hY>$?pP)<^Xijz`GNx!|fuB@l8y2mLW zANBU{&d$hGO-%6c>9nz|rlX!RC@9+5(`{#DmXeO&-`T9Gr03}2m6DA&E-genJHEZR z$;iT #f)ZD(bOgMeRHS2iv!BN-W^ zpqzVla&U5aXklMvUR*phGRw=wm6496uDW}6bJWw$v9GBxDJzSHgHcLJU|3gQSXNtF zUz(z=q@bLYk&RMHNU^S|v9GF#gn(aJR(o}GdUkSpb#p{LJC>1-U|3dPSy#En)KW`H zQA =PVH6Zy~rX7g7|>JU}gw~khPYLR$d;vMW0zVzck?Xmj=AVyXL@KUmCDe2~6Fe zJzC2?lXNP88Rz`xzj(yPeFj-xlq_J%`7eL<>)&MWpdNhq-v76m4<`j0ZGhP`&VT#6 zfIlZ8i$X?2LLv&li~=xx{3NA7qYbbZ{QeJr{L_nIo4A3?yIqfUcY4oh@aJCN4|aNk z$87gqWw(IW0Pn?LC0`TD2yyp`x6Sy=9$^7|Bs|8C 6UUjfv+I z9v=`A#2yLx;Q@U76oHSbfsX_J_m~*qsCVLaDL-+0K0oR99<9}Pmr)UK3Y_fsc<_&} z3Hc@>uM*<3ub+&+FyLa1USr^5jb3Bm egdOrtZvfh*-0u-w9GLS%}~ z(>3q~oOKW5!yWkBy{8w-xnFv11>iD`&H(S5?}vxQ(UA{8ylbAWfPuFsztSG(Cr#T_ zEDr-*7z&pImx{+-!)d<2u=jNTBt6|=7Ex=gSrnP?q4gavsq3n=1sqy6i0{pTm)|&4 z0#jhQ)Ve!Jap3$O b-Zt6x*=}wL~sX zl|cv{s>CPa9_~qcVIu}%bKt^IxEh$e 5@s{DJV zyHDKA>=7VK-Qmt2@PoU_sI@%bxI22w2HuFaULkofndw#l7cdz79tbxDR zz_F>oXazPmRy e2X09cbI%4&{W+plW6$_$7H}%AR*S~VQb{OW z3oQJq6vnd}K`krYm*(7*>OmL8dOoAk$^e@SOan(S(zwcATEK7kfhh>*KbX#J1blr* zU}!s)iwO$MzY`f;ycI;p*xpn(LQ13@I@BligEjQ;6QtS7J%+VmCcQEE`y^ zt74(<5e=;RR4qCl558p~>#LKv^uy!42*$eUI$Rn!R01 Vg7FR#0}S2c4Puwe5x$f*0;_bfATGfJX*dGE5pZcJTn#)> z%O_e2HKEfwL5bhn@i5ujkxkN3RwB%77wNOWWDh$haIu`8n+e_=Y$p;YmPRqz#|{i) z>BizsZbBOYS8DXbrFaqw=EXW@l$NAFA{ymm=EOe5^g(3R5S7)oER`C)hB>YBP +aq$4l1>L_}6J^`ZrGdR&rP%t%$>kORID>x#@h zmTzJy1TIW-f?MS&W!SNz>MGH|J=b-2bkD0}l*9TeDnj8L@F}i^;@c~gSt*o4;N);p z0Hj0zOq!@tG_Z6?a7BJ6fXfq^d8yNA3mEiSucPSZNT(vrCK8o7G{7OvBp;R7=}FR4 zPOx%XGL_};(|NKKSY(2rf|Y>lG&%+L?O316IZ1Uk5#?CsAr-JV^7LF+ezHr@nGP A03+UHkA{xVB#aEOlfh;HpqK3+!f8HNf!fSlfy{*=iI|W^BMpxtM%Z z?-?|(a(Z<{i7qHrVTWB=9{8B^Vqh=>2de>Bg~C~24ppb}jqfu|u!9nV^BY8{0Qh!6 zc)|(Nqsm|ExvnE29_#cZ(dYYQpe+$)3@Z!szz`Wtj{j8zj6KHS_}^4YEpKHgoB>Xj z?X0UzYl3OY9V>H~gjU{;*}ytI`9aqKOOr27@7R^)fKwB`1Pm7wYgwuVt_~b>z_~Xq zK=mKf>hv=7d14Hg);0~Cr~{*d45xZ8O#sTAXCBy$+`WOT0|y(JWmTGplnyoD(oq97 zP9S8LImIiLc+|kU@6tJUDQ9h_viCXQz?5DBej`=-JDnh_k?K%5JBPh==7G4LcWJB` zJn`H#7)N(M?ejWdY5uI$o620C4V>=BUINAiZ3UxMuhA(m3|9Ik8Z_|0H-k#;5sn40 zXC)u2fjz (XokP(gn!aHcXd zhc9b@@#R{w0&q PhEu+$cbSC!VwqVyp6FSZFgr>k>{jexV2^|?rL4$6Qt z7STGc{<%qQ;E)4ObC0o_q4LeVE5iZdWVsQQ_EzK(gMC%70kHC=Wo4poMV_)8IB-=n z8Uxn`4oko?_h_$n%uXj}GlvDq#~QIKV=LcXFLj}C5%5o08fo-80XNd -O1bHQ zG| zU;E~UC`fmk9LNw~ DHw Xrrsp1wo zkA=8x62pAd;B!VIlcJ|G)gu~@TU30Ln m7mnm!g>(+ zVM&VO*^aZ(1h`2koCU@)#i9BJ+r!fBHz}~p@dPS;Dds?5Dma!w;1^bIGGOT&CA4m? z<}8orM lN-_kwv%qKssx5I0Ta9G1aLMtnIB=D_bwaZ26nd8h&2j@bHEgL zCZm-JxUY7P)&oNqZHpfU=s9fVCVewmF$?JGyf-a*S=?}t3XNDpKROF6-ijd1K9{*U z3A`gmrUV$wRyPm{Jzp9zvrY*!CSNWdBciL9qm!aH>g5)zka?^0-r0Og=2cZ$x8bkO zKFFpEC Ygy*#N{{F5mg4~GwX;yCT8q?T&px)F4DcN+R#wRFV%TwrGalX! zk6Po~Tc<@fIzWF$oT1Yi4~L7+Cvr1;D!}(vXfOgJa!VFlTh3^DOE%8PoQy`(aqEms zN5t9Kbf)uNGIE{>@ck_ogu@Onewt5V5gtJYIt~$*;fx$PWOFp#n2yHszngk&Sn?ni zi;$Zs8Il_^U%*OCc5lcT84aNuV1aVT59H$~GMbT5`)L3#?# =}o$HP>6Q^6w@M+B9ds{3JFu=Rl }{ z(%E%3u1A|Uw_9`cvB&ccm + + sponsor +{% endblock %} From ed48cc457f36ea1f999bdf68668966935cd9e47f Mon Sep 17 00:00:00 2001 From: github-actions+ +
Date: Wed, 23 Jul 2025 13:02:50 +0000 Subject: [PATCH 08/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 83c06c013..42f9c5c2c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -16,6 +16,7 @@ hide: ### Internal +* 🔧 Update sponsors: Add Mobb. PR [#13916](https://github.com/fastapi/fastapi/pull/13916) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Experts. PR [#13889](https://github.com/fastapi/fastapi/pull/13889) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update FastAPI People sleep interval, use external settings. PR [#13888](https://github.com/fastapi/fastapi/pull/13888) by [@tiangolo](https://github.com/tiangolo). From da508e9dce85874d654eb10472b090f573de97fe Mon Sep 17 00:00:00 2001 From: Mohammad <116789737+Mohammad222PR@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:57:03 +0330 Subject: [PATCH 09/30] =?UTF-8?q?=F0=9F=8C=90=20Add=20Persian=20translatio?= =?UTF-8?q?n=20for=20`docs/fa/docs/environment-variables.md`=20(#13923)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🌐 Add Persian translation for docs/fa/docs/environment-variables.md --- docs/fa/docs/environment-variables.md | 298 ++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 docs/fa/docs/environment-variables.md diff --git a/docs/fa/docs/environment-variables.md b/docs/fa/docs/environment-variables.md new file mode 100644 index 000000000..75309ce1f --- /dev/null +++ b/docs/fa/docs/environment-variables.md @@ -0,0 +1,298 @@ +# متغیرهای محیطی + +/// tip + +اگه از قبل میدونی متغیرهای محیطی چی هستن و چطور ازشون استفاده میشه، میتونی این بخش رو رد کنی. + +/// + +یه متغیر محیطی (که بهش "**env var**" هم میگن) یه متغیریه که **خارج** از کد پایتون، توی **سیستمعامل** زندگی میکنه و میتونه توسط کد پایتونت (یا برنامههای دیگه) خونده بشه. + +متغیرهای محیطی میتونن برای مدیریت **تنظیمات** برنامه، بهعنوان بخشی از **نصب** پایتون و غیره مفید باشن. + +## ساخت و استفاده از متغیرهای محیطی + +میتونی متغیرهای محیطی رو توی **شل (ترمینال)** **بسازی** و ازشون استفاده کنی، بدون اینکه به پایتون نیاز داشته باشی: + +//// tab | لینوکس، مکاواس، ویندوز بش + + + +```console +// میتونی یه متغیر محیطی به اسم MY_NAME بسازی با +$ export MY_NAME="Wade Wilson" + +// بعد میتونی با برنامههای دیگه ازش استفاده کنی، مثل +$ echo "Hello $MY_NAME" + +Hello Wade Wilson +``` + ++ +//// + +//// tab | ویندوز پاورشل + ++ +```console +// یه متغیر محیطی به اسم MY_NAME بساز +$ $Env:MY_NAME = "Wade Wilson" + +// با برنامههای دیگه ازش استفاده کن، مثل +$ echo "Hello $Env:MY_NAME" + +Hello Wade Wilson +``` + ++ +//// + +## خوندن متغیرهای محیطی توی پایتون + +میتونی متغیرهای محیطی رو **خارج** از پایتون، توی ترمینال (یا با هر روش دیگه) بسازی، و بعد توی **پایتون** اونا رو بخونی. + +مثلاً میتونی یه فایل `main.py` داشته باشی با: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +/// tip + +آرگومان دوم `os.getenv()` مقدار پیشفرضیه که برمیگردونه. + +اگه ندی، بهصورت پیشفرض `None` هست، اینجا ما `"World"` رو بهعنوان مقدار پیشفرض گذاشتیم. + +/// + +بعد میتونی اون برنامه پایتون رو صدا کنی: + +//// tab | لینوکس، مکاواس، ویندوز بش + ++ +```console +// اینجا هنوز متغیر محیطی رو تنظیم نکردیم +$ python main.py + +// چون متغیر محیطی رو تنظیم نکردیم، مقدار پیشفرض رو میگیریم + +Hello World from Python + +// ولی اگه اول یه متغیر محیطی بسازیم +$ export MY_NAME="Wade Wilson" + +// و بعد دوباره برنامه رو صدا کنیم +$ python main.py + +// حالا میتونه متغیر محیطی رو بخونه + +Hello Wade Wilson from Python +``` + ++ +//// + +//// tab | ویندوز پاورشل + ++ +```console +// اینجا هنوز متغیر محیطی رو تنظیم نکردیم +$ python main.py + +// چون متغیر محیطی رو تنظیم نکردیم، مقدار پیشفرض رو میگیریم + +Hello World from Python + +// ولی اگه اول یه متغیر محیطی بسازیم +$ $Env:MY_NAME = "Wade Wilson" + +// و بعد دوباره برنامه رو صدا کنیم +$ python main.py + +// حالا میتونه متغیر محیطی رو بخونه + +Hello Wade Wilson from Python +``` + ++ +//// + +چون متغیرهای محیطی میتونن خارج از کد تنظیم بشن، ولی کد میتونه اونا رو بخونه، و لازم نیست با بقیه فایلها ذخیره (کمیتی به `git`) بشن، معمولاً برای پیکربندی یا **تنظیمات** استفاده میشن. + +همچنین میتونی یه متغیر محیطی رو فقط برای **یه اجرای خاص برنامه** بسازی، که فقط برای اون برنامه و فقط برای مدت زمان اجراش در دسترسه. + +برای این کار، درست قبل از خود برنامه، توی همون خط بسازش: + ++ +```console +// یه متغیر محیطی MY_NAME رو توی خط برای این اجرای برنامه بساز +$ MY_NAME="Wade Wilson" python main.py + +// حالا میتونه متغیر محیطی رو بخونه + +Hello Wade Wilson from Python + +// متغیر محیطی بعدش دیگه وجود نداره +$ python main.py + +Hello World from Python +``` + ++ +/// tip + +میتونی بیشتر در موردش توی برنامه دوازدهفاکتوری: پیکربندی بخونی. + +/// + +## نوعها و اعتبارسنجی + +این متغیرهای محیطی فقط میتونن **رشتههای متنی** رو نگه دارن، چون خارج از پایتون هستن و باید با برنامههای دیگه و بقیه سیستم (و حتی سیستمعاملهای مختلف مثل لینوکس، ویندوز، مکاواس) سازگار باشن. + +یعنی **هر مقداری** که توی پایتون از یه متغیر محیطی خونده میشه یه `str` هست، و هر تبدیل به نوع دیگه یا هر اعتبارسنجی باید توی کد انجام بشه. + +توی [راهنمای کاربر پیشرفته - تنظیمات و متغیرهای محیطی](./advanced/settings.md){.internal-link target=_blank} بیشتر در مورد استفاده از متغیرهای محیطی برای مدیریت **تنظیمات برنامه** یاد میگیری. + +## متغیر محیطی `PATH` + +یه متغیر محیطی **خاص** به اسم **`PATH`** وجود داره که سیستمعاملها (لینوکس، مکاواس، ویندوز) ازش برای پیدا کردن برنامههایی که قراره اجرا بشن استفاده میکنن. + +مقدار متغیر `PATH` یه رشته طولانی از پوشههاست که توی لینوکس و مکاواس با دونقطه `:` و توی ویندوز با نقطهویرگول `;` از هم جدا شدن. + +مثلاً، متغیر محیطی `PATH` میتونه اینجوری باشه: + +//// tab | لینوکس، مکاواس + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +``` + +یعنی سیستم باید دنبال برنامهها توی این پوشهها بگرده: + +* `/usr/local/bin` +* `/usr/bin` +* `/bin` +* `/usr/sbin` +* `/sbin` + +//// + +//// tab | ویندوز + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32 +``` + +یعنی سیستم باید دنبال برنامهها توی این پوشهها بگرده: + +* `C:\Program Files\Python312\Scripts` +* `C:\Program Files\Python312` +* `C:\Windows\System32` + +//// + +وقتی یه **دستور** توی ترمینال تایپ میکنی، سیستمعامل **دنبال** برنامه توی **هر کدوم از این پوشهها** که توی متغیر محیطی `PATH` لیست شدن میگرده. + +مثلاً، وقتی توی ترمینال `python` تایپ میکنی، سیستمعامل دنبال یه برنامه به اسم `python` توی **اولین پوشه** توی اون لیست میگرده. + +اگه پیداش کنه، **استفادهش میکنه**. وگرنه توی **پوشههای بعدی** دنبالش میگرده. + +### نصب پایتون و بهروزرسانی `PATH` + +وقتی پایتون رو نصب میکنی، ممکنه ازت بپرسن آیا میخوای متغیر محیطی `PATH` رو بهروزرسانی کنی. + +//// tab | لینوکس، مکاواس + +فرض کن پایتون رو نصب کردی و توی یه پوشه `/opt/custompython/bin` قرار گرفته. + +اگه بگی بله برای بهروزرسانی متغیر محیطی `PATH`، نصاب `/opt/custompython/bin` رو به متغیر محیطی `PATH` اضافه میکنه. + +ممکنه اینجوری بشه: + +```plaintext +/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/custompython/bin +``` + +اینجوری، وقتی توی ترمینال `python` تایپ میکنی، سیستم برنامه پایتون رو توی `/opt/custompython/bin` (آخرین پوشه) پیدا میکنه و از اون استفاده میکنه. + +//// + +//// tab | ویندوز + +فرض کن پایتون رو نصب کردی و توی یه پوشه `C:\opt\custompython\bin` قرار گرفته. + +اگه بگی بله برای بهروزرسانی متغیر محیطی `PATH`، نصاب `C:\opt\custompython\bin` رو به متغیر محیطی `PATH` اضافه میکنه. + +```plaintext +C:\Program Files\Python312\Scripts;C:\Program Files\Python312;C:\Windows\System32;C:\opt\custompython\bin +``` + +اینجوری، وقتی توی ترمینال `python` تایپ میکنی، سیستم برنامه پایتون رو توی `C:\opt\custompython\bin` (آخرین پوشه) پیدا میکنه و از اون استفاده میکنه. + +//// + +پس، اگه تایپ کنی: + ++ +```console +$ python +``` + ++ +//// tab | لینوکس، مکاواس + +سیستم برنامه `python` رو توی `/opt/custompython/bin` **پیدا** میکنه و اجراش میکنه. + +تقریباً معادل اینه که تایپ کنی: + ++ +```console +$ /opt/custompython/bin/python +``` + ++ +//// + +//// tab | ویندوز + +سیستم برنامه `python` رو توی `C:\opt\custompython\bin\python` **پیدا** میکنه و اجراش میکنه. + +تقریباً معادل اینه که تایپ کنی: + ++ +```console +$ C:\opt\custompython\bin\python +``` + ++ +//// + +این اطلاعات وقتی در مورد [محیطهای مجازی](virtual-environments.md){.internal-link target=_blank} یاد میگیری بهدردت میخوره. + +## نتیجهگیری + +با این باید یه درک پایهای از **متغیرهای محیطی** و نحوه استفادهشون توی پایتون داشته باشی. + +میتونی بیشتر در موردشون توی ویکیپدیا برای متغیر محیطی بخونی. + +توی خیلی موارد مشخص نیست که متغیرهای محیطی چطور میتونن فوری مفید و کاربردی باشن. ولی توی موقعیتهای مختلف توسعه مدام پیداشون میشه، پس خوبه که در موردشون بدونی. + +مثلاً، توی بخش بعدی در مورد [محیطهای مجازی](virtual-environments.md) به این اطلاعات نیاز داری. From 4ca93aea04bf289cedb4dc0d922c97f2e99399d9 Mon Sep 17 00:00:00 2001 From: github-actionsDate: Fri, 25 Jul 2025 09:27:24 +0000 Subject: [PATCH 10/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 42f9c5c2c..e198e9437 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Add Persian translation for `docs/fa/docs/environment-variables.md`. PR [#13923](https://github.com/fastapi/fastapi/pull/13923) by [@Mohammad222PR](https://github.com/Mohammad222PR). * 🌐 Add Persian translation for `docs/fa/docs/python-types.md`. PR [#13524](https://github.com/fastapi/fastapi/pull/13524) by [@Mohammad222PR](https://github.com/Mohammad222PR). * 🌐 Update Portuguese Translation for `docs/pt/docs/project-generation.md`. PR [#13875](https://github.com/fastapi/fastapi/pull/13875) by [@EdmilsonRodrigues](https://github.com/EdmilsonRodrigues). * 🌐 Add Persian translation for `docs/fa/docs/async.md`. PR [#13541](https://github.com/fastapi/fastapi/pull/13541) by [@Mohammad222PR](https://github.com/Mohammad222PR). From e4453a850d794fd211eb3741c85cf3bec320c871 Mon Sep 17 00:00:00 2001 From: Koyo Miyazaki <82708304+KoyoMiyazaki@users.noreply.github.com> Date: Sat, 26 Jul 2025 19:48:11 +0900 Subject: [PATCH 11/30] =?UTF-8?q?=F0=9F=93=9D=20Fix=20highlight=20line=20i?= =?UTF-8?q?n=20`docs/ja/docs/tutorial/body.md`=20(#13927)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix highlight line in docs/ja/docs/tutorial/body.md --- docs/ja/docs/tutorial/body.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/ja/docs/tutorial/body.md b/docs/ja/docs/tutorial/body.md index 8376959d5..1298eec7e 100644 --- a/docs/ja/docs/tutorial/body.md +++ b/docs/ja/docs/tutorial/body.md @@ -22,7 +22,7 @@ GET リクエストでボディを送信することは、仕様では未定義 ます初めに、 `pydantic` から `BaseModel` をインポートする必要があります: -{* ../../docs_src/body/tutorial001.py hl[2] *} +{* ../../docs_src/body/tutorial001.py hl[4] *} ## データモデルの作成 @@ -30,7 +30,7 @@ GET リクエストでボディを送信することは、仕様では未定義 すべての属性にpython標準の型を使用します: -{* ../../docs_src/body/tutorial001.py hl[5:9] *} +{* ../../docs_src/body/tutorial001.py hl[7:11] *} クエリパラメータの宣言と同様に、モデル属性がデフォルト値をもつとき、必須な属性ではなくなります。それ以外は必須になります。オプショナルな属性にしたい場合は `None` を使用してください。 @@ -58,7 +58,7 @@ GET リクエストでボディを送信することは、仕様では未定義 *パスオペレーション* に加えるために、パスパラメータやクエリパラメータと同じ様に宣言します: -{* ../../docs_src/body/tutorial001.py hl[16] *} +{* ../../docs_src/body/tutorial001.py hl[18] *} ...そして、作成したモデル `Item` で型を宣言します。 @@ -125,7 +125,7 @@ GET リクエストでボディを送信することは、仕様では未定義 関数内部で、モデルの全ての属性に直接アクセスできます: -{* ../../docs_src/body/tutorial002.py hl[19] *} +{* ../../docs_src/body/tutorial002.py hl[21] *} ## リクエストボディ + パスパラメータ @@ -133,7 +133,7 @@ GET リクエストでボディを送信することは、仕様では未定義 **FastAPI** はパスパラメータである関数パラメータは**パスから受け取り**、Pydanticモデルによって宣言された関数パラメータは**リクエストボディから受け取る**ということを認識します。 -{* ../../docs_src/body/tutorial003.py hl[15:16] *} +{* ../../docs_src/body/tutorial003.py hl[17:18] *} ## リクエストボディ + パスパラメータ + クエリパラメータ @@ -141,7 +141,7 @@ GET リクエストでボディを送信することは、仕様では未定義 **FastAPI** はそれぞれを認識し、適切な場所からデータを取得します。 -{* ../../docs_src/body/tutorial004.py hl[16] *} +{* ../../docs_src/body/tutorial004.py hl[18] *} 関数パラメータは以下の様に認識されます: From 106a4338db8d546f3a6bf817e237331297e7f797 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Jul 2025 10:48:36 +0000 Subject: [PATCH 12/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e198e9437..d5186aad3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 📝 Fix highlight line in `docs/ja/docs/tutorial/body.md`. PR [#13927](https://github.com/fastapi/fastapi/pull/13927) by [@KoyoMiyazaki](https://github.com/KoyoMiyazaki). * 🌐 Add Persian translation for `docs/fa/docs/environment-variables.md`. PR [#13923](https://github.com/fastapi/fastapi/pull/13923) by [@Mohammad222PR](https://github.com/Mohammad222PR). * 🌐 Add Persian translation for `docs/fa/docs/python-types.md`. PR [#13524](https://github.com/fastapi/fastapi/pull/13524) by [@Mohammad222PR](https://github.com/Mohammad222PR). * 🌐 Update Portuguese Translation for `docs/pt/docs/project-generation.md`. PR [#13875](https://github.com/fastapi/fastapi/pull/13875) by [@EdmilsonRodrigues](https://github.com/EdmilsonRodrigues). From 54c7c34b25e9c71d218f4a01095520bc2b582b24 Mon Sep 17 00:00:00 2001 From: Chih-Hsuan Yen <1937689+yan12125@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:49:23 +0800 Subject: [PATCH 13/30] =?UTF-8?q?=E2=AC=86=20Update=20httpx=20requirement?= =?UTF-8?q?=20to=20>=3D0.23.0,<0.29.0=20(#13114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- requirements-docs-tests.txt | 2 +- requirements-github-actions.txt | 2 +- .../test_custom_request_and_route/test_tutorial002.py | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt index 71f4a7ab9..e7684a2e3 100644 --- a/requirements-docs-tests.txt +++ b/requirements-docs-tests.txt @@ -1,4 +1,4 @@ # For mkdocstrings and tests -httpx >=0.23.0,<0.28.0 +httpx >=0.23.0,<0.29.0 # For linting and generating docs versions ruff ==0.11.2 diff --git a/requirements-github-actions.txt b/requirements-github-actions.txt index 920aefea6..f807d06a8 100644 --- a/requirements-github-actions.txt +++ b/requirements-github-actions.txt @@ -1,6 +1,6 @@ PyGithub>=2.3.0,<3.0.0 pydantic>=2.5.3,<3.0.0 pydantic-settings>=2.1.0,<3.0.0 -httpx>=0.27.0,<0.28.0 +httpx>=0.27.0,<0.29.0 pyyaml >=5.3.1,<7.0.0 smokeshow diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index 6f7355aaa..647f1c5dd 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.custom_request_and_route.tutorial002 import app @@ -24,14 +24,16 @@ def test_exception_handler_body_access(): "input": {"numbers": [1, 2, 3]}, } ], - "body": '{"numbers": [1, 2, 3]}', + # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 + "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), } } ) | IsDict( # TODO: remove when deprecating Pydantic v1 { "detail": { - "body": '{"numbers": [1, 2, 3]}', + # httpx 0.28.0 switches to compact JSON https://github.com/encode/httpx/issues/3363 + "body": IsOneOf('{"numbers": [1, 2, 3]}', '{"numbers":[1,2,3]}'), "errors": [ { "loc": ["body"], From bfbed308699d6f8204411e5bbaaa084b6e779c30 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Jul 2025 10:49:51 +0000 Subject: [PATCH 14/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d5186aad3..c75a8b574 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* ⬆ Update httpx requirement to >=0.23.0,<0.29.0. PR [#13114](https://github.com/fastapi/fastapi/pull/13114) by [@yan12125](https://github.com/yan12125). * 🔧 Update sponsors: Add Mobb. PR [#13916](https://github.com/fastapi/fastapi/pull/13916) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Experts. PR [#13889](https://github.com/fastapi/fastapi/pull/13889) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update FastAPI People sleep interval, use external settings. PR [#13888](https://github.com/fastapi/fastapi/pull/13888) by [@tiangolo](https://github.com/tiangolo). From fcdd5031e1b150952bd70ea8d1ea805d5f1014c6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 12:51:31 +0200 Subject: [PATCH 15/30] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-commit?= =?UTF-8?q?=20autoupdate=20(#13894)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.2 → v0.12.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.2...v0.12.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 680cafce9..e461b0133 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.2 + rev: v0.12.4 hooks: - id: ruff args: From 8bfc783194bdfc2142ac716ae208c735e1862c81 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Jul 2025 10:51:53 +0000 Subject: [PATCH 16/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c75a8b574..d5730e247 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Internal +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#13894](https://github.com/fastapi/fastapi/pull/13894) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Update httpx requirement to >=0.23.0,<0.29.0. PR [#13114](https://github.com/fastapi/fastapi/pull/13114) by [@yan12125](https://github.com/yan12125). * 🔧 Update sponsors: Add Mobb. PR [#13916](https://github.com/fastapi/fastapi/pull/13916) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People - Experts. PR [#13889](https://github.com/fastapi/fastapi/pull/13889) by [@tiangolo](https://github.com/tiangolo). From 2013336d65e1b7ac25aa3a4b130d41b6b2ac31f4 Mon Sep 17 00:00:00 2001 From: Edmilson Monteiro Rodrigues Neto Date: Sat, 26 Jul 2025 12:16:39 +0100 Subject: [PATCH 17/30] =?UTF-8?q?=20=F0=9F=8C=90=20Update=20Portuguese=20T?= =?UTF-8?q?ranslation=20for=20`docs/pt/docs/async.md`=20(#13863)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/async.md | 207 ++++++++++++++++++++++-------------------- 1 file changed, 108 insertions(+), 99 deletions(-) diff --git a/docs/pt/docs/async.md b/docs/pt/docs/async.md index 0d6bdbf0e..4425eba77 100644 --- a/docs/pt/docs/async.md +++ b/docs/pt/docs/async.md @@ -40,7 +40,7 @@ def results(): --- -Se sua aplicação (de alguma forma) não tem que se comunicar com nada mais e tem que esperar que o respondam, use `async def`. +Se sua aplicação (de alguma forma) não tem que se comunicar com nada mais e esperar que o respondam, use `async def`. --- @@ -52,7 +52,7 @@ Se você simplesmente não sabe, use apenas `def`. De qualquer forma, em ambos os casos acima, FastAPI irá trabalhar assincronamente e ser extremamente rápido. -Seguindo os passos acima, ele será capaz de fazer algumas otimizações de performance. +Mas, seguindo os passos acima, ele será capaz de fazer algumas otimizações de performance. ## Detalhes Técnicos @@ -66,36 +66,36 @@ Vamos ver aquela frase por partes na seção abaixo: ## Código assíncrono -Código assíncrono apenas significa que a linguagem 💬 tem um jeito de dizer para o computador / programa 🤖 que em certo ponto, ele 🤖 terá que esperar por *algo* para finalizar em outro lugar. Vamos dizer que esse *algo* seja chamado "arquivo lento" 📝. +Código assíncrono apenas significa que a linguagem 💬 tem um jeito de dizer para o computador / programa 🤖 que em certo ponto do código, ele 🤖 terá que esperar *algo* finalizar em outro lugar. Vamos dizer que esse *algo* seja chamado "arquivo lento" 📝. -Então, durante esse tempo, o computador pode ir e fazer outro trabalho, enquanto o "arquivo lento" 📝 termine. +Então, durante esse tempo, o computador pode ir e fazer outro trabalho, enquanto o "arquivo lento" 📝 termina. -Então o computador / programa 🤖 irá voltar toda hora que tiver uma chance porquê ele ainda está esperando o "arquivo lento", ou ele 🤖 nunca irá terminar todo o trabalho que tem até esse ponto. E ele 🤖 irá ver se alguma das tarefas que estava esperando já terminaram, fazendo o que quer que tinham que fazer. +Então o computador / programa 🤖 irá voltar sempre que tiver uma chance, seja porque ele está esperando novamente, ou quando ele 🤖 terminar todo o trabalho que tem até esse ponto. E ele 🤖 irá ver se alguma das tarefas que estava esperando já terminaram de fazer o que quer que tinham que fazer. -Depois, ele 🤖 pega a primeira tarefa para finalizar (vamos dizer, nosso "arquivo lento" 📝) e continua o que ele tem que fazer com isso. +Depois, ele 🤖 pega a primeira tarefa para finalizar (vamos dizer, nosso "arquivo lento" 📝) e continua o que tem que fazer com ela. -Esse "esperar por algo" normalmente se refere a operações I/O que são relativamente "lentas" (comparadas a velocidade do processador e da memória RAM), como esperar por: +Esse "esperar por algo" normalmente se refere a operações I/O que são relativamente "lentas" (comparadas à velocidade do processador e da memória RAM), como esperar por: * dados do cliente para serem enviados através da rede -* dados enviados pelo seu programa para serem recebidos pelo clente através da rede -* conteúdo de um arquivo no disco pra ser lido pelo sistema e entregar ao seu programa +* dados enviados pelo seu programa serem recebidos pelo clente através da rede +* conteúdo de um arquivo no disco ser lido pelo sistema e entregue ao seu programa * conteúdo que seu programa deu ao sistema para ser escrito no disco -* uma operação remota API -* uma operação no banco de dados para finalizar -* uma solicitação no banco de dados esperando o retorno do resultado +* uma operação em uma API remota +* uma operação no banco de dados finalizar +* uma solicitação no banco de dados retornar o resultado * etc. -Enquanto o tempo de execução é consumido mais pela espera das operações I/O, essas operações são chamadas de operações "limitadas por I/O". +Quanto o tempo de execução é consumido majoritariamente pela espera de operações I/O, essas operações são chamadas operações "limitadas por I/O". -Isso é chamado de "assíncrono" porquê o computador / programa não tem que ser "sincronizado" com a tarefa lenta, esperando pelo exato momento que a tarefa finalize, enquanto não faz nada, para ser capaz de pegar o resultado da tarefa e dar continuidade ao trabalho. +Isso é chamado de "assíncrono" porque o computador / programa não tem que ser "sincronizado" com a tarefa lenta, esperando pelo momento exato em que a tarefa finaliza, enquanto não faz nada, para ser capaz de pegar o resultado da tarefa e dar continuidade ao trabalho. -Ao invés disso, sendo um sistema "assíncrono", uma vez finalizada, a tarefa pode esperar um pouco (alguns microssegundos) para que o computador / programa finalize o que quer que esteja fazendo,e então volte para pegar o resultado e continue trabalhando com ele. +Ao invés disso, sendo um sistema "assíncrono", uma vez finalizada, a tarefa pode esperar na fila um pouco (alguns microssegundos) para que o computador / programa finalize o que quer que esteja fazendo, e então volte para pegar o resultado e continue trabalhando com ele. -Para "síncrono" (contrário de "assíncrono") também é utilizado o termo "sequencial", porquê o computador / programa segue todos os passos, na sequência, antes de trocar para uma tarefa diferente, mesmo se alguns passos envolvam esperar. +Para "síncrono" (contrário de "assíncrono") também é utilizado o termo "sequencial", porquê o computador / programa segue todos os passos, em sequência, antes de trocar para uma tarefa diferente, mesmo se alguns passos envolvam esperar. ### Concorrência e hambúrgueres -Essa idéia de código **assíncrono** descrito acima é algo às vezes chamado de **"concorrência"**. E é diferente de **"paralelismo"**. +Essa idéia de código **assíncrono** descrita acima é às vezes chamado de **"concorrência"**. Isso é diferente de **"paralelismo"**. **Concorrência** e **paralelismo** ambos são relacionados a "diferentes coisas acontecendo mais ou menos ao mesmo tempo". @@ -105,117 +105,115 @@ Para ver essa diferença, imagine a seguinte história sobre hambúrgueres: ### Hambúrgueres concorrentes -Você vai com seu _crush_ :heart_eyes: na lanchonete, fica na fila enquanto o caixa pega os pedidos das pessoas na sua frente. +Você vai com seu _crush_ na lanchonete, e fica na fila enquanto o caixa pega os pedidos das pessoas na sua frente. 😍 -Então chega a sua vez, você pede dois saborosos hambúrgueres para você e seu _crush_ :heart_eyes:. +Então chega a sua vez, você pede dois saborosos hambúrgueres para você e seu _crush_. 🍔🍔 -Você paga. +O caixa diz alguma coisa para o cozinheiro na cozinha para que eles saivam que têm que preparar seus hambúrgueres (mesmo que ele esteja atualmente preparando os lanches dos outros clientes). -O caixa diz alguma coisa para o cara na cozinha para que ele tenha que preparar seus hambúrgueres (mesmo embora ele esteja preparando os lanches dos outros clientes). +Você paga. 💸 O caixa te entrega seu número de chamada. -Enquanto você espera, você vai com seu _crush_ :heart_eyes: e pega uma mesa, senta e conversa com seu _crush_ :heart_eyes: por um bom tempo (como seus hambúrgueres são muito saborosos, leva um tempo para serem preparados). +Enquanto você espera, você vai com seu _crush_ e pega uma mesa, senta e conversa com seu _crush_ por um bom tempo (já que seus hambúrgueres são muito saborosos, e leva um tempo para serem preparados). -Enquanto você está sentado na mesa com seu _crush_ :heart_eyes:, esperando os hambúrgueres, você pode gastar o tempo admirando como lindo, maravilhoso e esperto é seu _crush_ :heart_eyes:. +Já que você está sentado na mesa com seu _crush_, esperando os hambúrgueres, você pode passar esse tempo admirando o quão lindo, maravilhoso e esperto é seu _crush_ ✨😍✨. -Enquanto espera e conversa com seu _crush_ :heart_eyes:, de tempos em tempos, você verifica o número de chamada exibido no balcão para ver se já é sua vez. +Enquanto espera e conversa com seu _crush_, de tempos em tempos, você verifica o número da chamada exibido no balcão para ver se já é sua vez. -Então a certo ponto, é finalmente sua vez. Você vai no balcão, pega seus hambúrgueres e volta para a mesa. +Então em algum momento, é finalmente sua vez. Você vai ao balcão, pega seus hambúrgueres e volta para a mesa. -Você e seu _crush_ :heart_eyes: comem os hambúrgueres e aproveitam o tempo. +Você e seu _crush_ comem os hambúrgueres e aproveitam o tempo. ✨ --- -Imagine que você seja o computador / programa nessa história. +Imagine que você seja o computador / programa nessa história. -Enquanto você está na fila, tranquilo, esperando por sua vez, não está fazendo nada "produtivo". Mas a fila é rápida porquê o caixa só está pegando os pedidos, então está tudo bem. +Enquanto você está na fila, você está somente ocioso 😴, esperando por sua vez, sem fazer nada muito "produtivo". Mas a fila é rápida porque o caixa só está pegando os pedidos (não os preparando), então está tudo bem. -Então, quando é sua vez, você faz o trabalho "produtivo" de verdade, você processa o menu, decide o que quer, pega a escolha de seu _crush_ :heart_eyes:, paga, verifica se entregou o valor correto em dinheiro ou cartão de crédito, verifica se foi cobrado corretamente, verifica se seu pedido está correto etc. +Então, quando é sua vez, você faz trabalho realmente "produtivo", você processa o menu, decide o que quer, pega a escolha de seu _crush_, paga, verifica se entregou o cartão ou a cédula correta, verifica se foi cobrado corretamente, verifica se seu pedido está correto etc. -Mas então, embora você ainda não tenha os hambúrgueres, seu trabalho no caixa está "pausado", porquê você tem que esperar seus hambúrgueres estarem prontos. +Mas então, embora você ainda não tenha os hambúrgueres, seu trabalho no caixa está "pausado" ⏸, porque você tem que esperar 🕙 seus hambúrgueres ficarem prontos. -Mas enquanto você se afasta do balcão e senta na mesa com o número da sua chamada, você pode trocar sua atenção para seu _crush_ :heart_eyes:, e "trabalhar" nisso. Então você está novamente fazendo algo muito "produtivo", como flertar com seu _crush_ :heart_eyes:. +Contudo, à medida que você se afasta do balcão e senta na mesa, com um número para sua chamada, você pode trocar 🔀 sua atenção para seu _crush_, e "trabalhar" ⏯ 🤓 nisso. Então você está novamente fazendo algo muito "produtivo", como flertar com seu _crush_ 😍. -Então o caixa diz que "seus hambúrgueres estão prontos" colocando seu número no balcão, mas você não corre que nem um maluco imediatamente quando o número exibido é o seu. Você sabe que ninguém irá roubar seus hambúrgueres porquê você tem o número de chamada, e os outros tem os números deles. +Então o caixa 💁 diz que "seus hambúrgueres estão prontos" colocando seu número no balcão, mas você não corre que nem um maluco imediatamente quando o número exibido é o seu. Você sabe que ninguém irá roubar seus hambúrgueres porque você tem o seu número da chamada, e os outros têm os deles. -Então você espera que seu _crush_ :heart_eyes: termine a história que estava contando (terminar o trabalho atual / tarefa sendo processada), sorri gentilmente e diz que você está indo buscar os hambúrgueres. +Então você espera seu _crush_ terminar a história que estava contando (terminar o trabalho atual ⏯ / tarefa sendo processada 🤓), sorri gentilmente e diz que você está indo buscar os hambúrgueres. -Então você vai no balcão, para a tarefa inicial que agora está finalizada, pega os hambúrgueres, e leva para a mesa. Isso finaliza esse passo / tarefa da interação com o balcão. Agora é criada uma nova tarefa, "comer hambúrgueres", mas a tarefa anterior, "pegar os hambúrgueres" já está finalizada. +Então você vai ao balcão 🔀, para a tarefa inicial que agora está finalizada⏯, pega os hambúrgueres, agradece, e leva-os para a mesa. Isso finaliza esse passo / tarefa da interação com o balcão ⏹. Isso, por sua vez, cria uma nova tarefa, a de "comer hambúrgueres" 🔀 ⏯, mas a tarefa anterior de "pegar os hambúrgueres" já está finalizada ⏹. ### Hambúrgueres paralelos -Você vai com seu _crush_ :heart_eyes: em uma lanchonete paralela. +Agora vamos imaginar que esses não são "Hambúrgueres Concorrentes", e sim "Hambúrgueres Paralelos" -Você fica na fila enquanto alguns (vamos dizer 8) caixas pegam os pedidos das pessoas na sua frente. +Você vai com seu _crush_ na lanchonete paralela. -Todo mundo antes de você está esperando pelos hambúrgueres estarem prontos antes de deixar o caixa porquê cada um dos 8 caixas vai e prepara o hambúrguer antes de pegar o próximo pedido. +Você fica na fila enquanto vários (vamos dizer 8) caixas que também são cozinheiros pegam os pedidos das pessoas na sua frente. -Então é finalmente sua vez, e pede 2 hambúrgueres muito saborosos para você e seu _crush_ :heart_eyes:. +Todo mundo na sua frente está esperando seus hambúrgueres ficarem prontos antes de deixar o caixa porque cada um dos 8 caixas vai e prepara o hambúrguer logo após receber o pedido, antes de pegar o próximo pedido. -Você paga. +Então é finalmente sua vez, você pede 2 hambúrgueres muito saborosos para você e seu _crush_. + +Você paga 💸. O caixa vai para a cozinha. -Você espera, na frente do balcão, para que ninguém pegue seus hambúrgueres antes de você, já que não tem números de chamadas. +Você espera, na frente do balcão 🕙, para que ninguém pegue seus hambúrgueres antes de você, já que não tem números de chamadas. -Enquanto você e seu _crush_ :heart_eyes: estão ocupados não permitindo que ninguém passe a frente e pegue seus hambúrgueres assim que estiverem prontos, você não pode dar atenção ao seu _crush_ :heart_eyes:. +Como você e seu _crush_ estão ocupados não permitindo que ninguém passe na frente e pegue seus hambúrgueres assim que estiverem prontos, você não pode dar atenção ao seu _crush_. 😞 -Isso é trabalho "síncrono", você está "sincronizado" com o caixa / cozinheiro. Você tem que esperar e estar lá no exato momento que o caixa / cozinheiro terminar os hambúrgueres e dá-los a você, ou então, outro alguém pode pegá-los. +Isso é trabalho "síncrono", você está "sincronizado" com o caixa / cozinheiro👨🍳. Você tem que esperar 🕙 e estar lá no exato momento que o caixa / cozinheiro 👨🍳 terminar os hambúrgueres e os der a você, ou então, outro alguém pode pegá-los. -Então seu caixa / cozinheiro finalmente volta com seus hambúrgueres, depois de um longo tempo esperando por eles em frente ao balcão. +Então seu caixa / cozinheiro 👨🍳 finalmente volta com seus hambúrgueres, depois de um longo tempo esperando 🕙 por eles em frente ao balcão. -Você pega seus hambúrgueres e vai para a mesa com seu _crush_ :heart_eyes:. +Você pega seus hambúrgueres e vai para a mesa com seu _crush_. -Vocês comem os hambúrgueres, e o trabalho está terminado. +Vocês comem os hambúrgueres, e o trabalho está terminado. ⏹ -Não houve muita conversa ou flerte já que a maior parte do tempo foi gasto esperando os lanches na frente do balcão. +Não houve muita conversa ou flerte já que a maior parte do tempo foi gasto esperando 🕙 na frente do balcão. 😞 --- -Nesse cenário dos hambúrgueres paralelos, você é um computador / programa com dois processadores (você e seu _crush_ :heart_eyes:), ambos esperando e dedicando a atenção de estar "esperando no balcão" por um bom tempo. +Nesse cenário dos hambúrgueres paralelos, você é um computador / programa com dois processadores (você e seu _crush_), ambos esperando 🕙 e dedicando sua atenção ⏯ "esperando no balcão" 🕙 por um bom tempo. -A lanchonete paralela tem 8 processadores (caixas / cozinheiros). Enquanto a lanchonete dos hambúrgueres concorrentes tinham apenas 2 (um caixa e um cozinheiro). +A lanchonete paralela tem 8 processadores (caixas / cozinheiros), enquanto a lanchonete dos hambúrgueres concorrentes tinha apenas 2 (um caixa e um cozinheiro). -Ainda assim, a última experiência não foi a melhor. +Ainda assim, a experiência final não foi a melhor. 😞 --- -Essa poderia ser a história paralela equivalente aos hambúrgueres. +Essa seria o equivalente paralelo à histório dos hambúrgueres. 🍔 Para um exemplo "mais real", imagine um banco. -Até recentemente, a maioria dos bancos tinha muitos caixas e uma grande fila. +Até recentemente, a maioria dos bancos tinham muitos caixas 👨💼👨💼👨💼👨💼 e uma grande fila 🕙🕙🕙🕙🕙🕙🕙🕙. -Todos os caixas fazendo todo o trabalho, um cliente após o outro. +Todos os caixas fazendo todo o trabalho, um cliente após o outro 👨💼⏯. -E você tinha que esperar na fila por um longo tempo ou poderia perder a vez. +E você tinha que esperar 🕙 na fila por um longo tempo ou poderia perder a vez. -Você provavelmente não gostaria de levar seu _crush_ :heart_eyes: com você para um rolezinho no banco. +Você provavelmente não gostaria de levar seu _crush_ 😍 com você para um rolezinho no banco 🏦. ### Conclusão dos hambúrgueres -Nesse cenário dos "hambúrgueres com seu _crush_ :heart_eyes:", como tem muita espera, faz mais sentido ter um sistema concorrente. +Nesse cenário dos "hambúrgueres com seu _crush_", como tem muita espera, faz mais sentido ter um sistema concorrente ⏸🔀⏯. Esse é o caso da maioria das aplicações web. -Geralmente são muitos usuários, e seu servidor está esperando pelas suas conexões não tão boas para enviar as requisições. - -E então esperando novamente pelas respostas voltarem. - -Essa "espera" é medida em microssegundos, e ainda assim, somando tudo, é um monte de espera no final. +Muitos, muitos usuários, mas seu servidor está esperando 🕙 pela sua conexão não tão boa enviar suas requisições. -Por isso que faz muito mais sentido utilizar código assíncrono para APIs web. +E então esperando 🕙 novamente as respostas voltarem. -A maioria dos frameworks Python existentes mais populares (incluindo Flask e Django) foram criados antes que os novos recursos assíncronos existissem em Python. Então, os meios que eles podem ser colocados em produção para suportar execução paralela mais a forma antiga de execução assíncrona não são tão poderosos quanto as novas capacidades. +Essa "espera" 🕙 é medida em microssegundos, mas ainda assim, somando tudo, é um monte de espera no final. -Mesmo embora a especificação principal para web assíncrono em Python (ASGI) foi desenvolvida no Django, para adicionar suporte para WebSockets. +Por isso que faz bastante sentido utilizar código assíncrono ⏸🔀⏯ para APIs web. -Esse tipo de assincronicidade é o que fez NodeJS popular (embora NodeJS não seja paralelo) e que essa seja a força do Go como uma linguagem de programa. +Esse tipo de assincronicidade é o que fez NodeJS popular (embora NodeJS não seja paralelo) e essa é a força do Go como uma linguagem de programação. E esse é o mesmo nível de performance que você tem com o **FastAPI**. -E como você pode ter paralelismo e sincronicidade ao mesmo tempo, você tem uma maior performance do que a maioria dos frameworks NodeJS testados e lado a lado com Go, que é uma linguagem compilada próxima ao C (tudo graças ao Starlette). +E como você pode ter paralelismo e assincronicidade ao mesmo tempo, você tem uma maior performance do que a maioria dos frameworks NodeJS testados e lado a lado com Go, que é uma linguagem compilada, mais próxima ao C (tudo graças ao Starlette). ### Concorrência é melhor que paralelismo? @@ -225,64 +223,64 @@ Concorrência é diferente de paralelismo. E é melhor em cenários **específic Então, para equilibrar tudo, imagine a seguinte historinha: -> Você tem que limpar uma grande casa suja. +> Você tem que limpar uma casa grande e suja. *Sim, essa é toda a história*. --- -Não há espera em lugar algum, apenas um monte de trabalho para ser feito, em múltiplos cômodos da casa. +Não há espera 🕙 em lugar algum, apenas um monte de trabalho para ser feito, em múltiplos cômodos da casa. -Você poderia ter chamadas como no exemplo dos hambúrgueres, primeiro a sala de estar, então a cozinha, mas você não está esperando por nada, apenas limpar e limpar, as chamadas não afetariam em nada. +Você poderia ter turnos como no exemplo dos hambúrgueres, primeiro a sala de estar, então a cozinha, mas como você não está esperando por nada, apenas limpando e limpando, as chamadas não afetariam em nada. -Levaria o mesmo tempo para finalizar com ou sem chamadas (concorrência) e você teria feito o mesmo tanto de trabalho. +Levaria o mesmo tempo para finalizar com ou sem turnos (concorrência) e você teria feito o mesmo tanto de trabalho. Mas nesse caso, se você trouxesse os 8 ex-caixas / cozinheiros / agora-faxineiros, e cada um deles (mais você) pudessem dividir a casa para limpá-la, vocês fariam toda a limpeza em **paralelo**, com a ajuda extra, e terminariam muito mais cedo. Nesse cenário, cada um dos faxineiros (incluindo você) poderia ser um processador, fazendo a sua parte do trabalho. -E a maior parte do tempo de execução é tomada por trabalho (ao invés de ficar esperando), e o trabalho em um computador é feito pela CPU, que podem gerar problemas que são chamados de "limite de CPU". +E a maior parte do tempo de execução é tomada por trabalho real (ao invés de ficar esperando), e o trabalho em um computador é feito pela CPU. Eles chamam esses problemas de "limitados por CPU". --- -Exemplos comuns de limite de CPU são coisas que exigem processamento matemático complexo. +Exemplos comuns de operações limitadas por CPU são coisas que exigem processamento matemático complexo. Por exemplo: * **Processamento de áudio** ou **imagem** -* **Visão do Computador**: uma imagem é composta por milhões de pixels, cada pixel tem 3 valores (cores, processamento que normalmente exige alguma computação em todos esses pixels ao mesmo tempo) +* **Visão Computacional**: uma imagem é composta por milhões de pixels, cada pixel tem 3 valores / cores, processar isso normalmente exige alguma computação em todos esses pixels ao mesmo tempo -* **Machine Learning**: Normalmente exige muita multiplicação de matrizes e vetores. Pense numa grande folha de papel com números e multiplicando todos eles juntos e ao mesmo tempo. +* **Machine Learning**: Normalmente exige muita multiplicação de matrizes e vetores. Pense numa grande planilha com números e em multiplicar todos eles juntos e ao mesmo tempo. -* **Deep Learning**: Esse é um subcampo do Machine Learning, então o mesmo se aplica. A diferença é que não há apenas uma grande folha de papel com números para multiplicar, mas um grande conjunto de folhas de papel, e em muitos casos, você utiliza um processador especial para construir e/ou usar modelos. +* **Deep Learning**: Esse é um subcampo do Machine Learning, então, o mesmo se aplica. A diferença é que não há apenas uma grande planilha com números para multiplicar, mas um grande conjunto delas, e em muitos casos, você utiliza um processador especial para construir e/ou usar esses modelos. ### Concorrência + Paralelismo: Web + Machine learning Com **FastAPI** você pode levar a vantagem da concorrência que é muito comum para desenvolvimento web (o mesmo atrativo de NodeJS). -Mas você também pode explorar os benefícios do paralelismo e multiprocessamento (tendo múltiplos processadores rodando em paralelo) para trabalhos pesados que geram **limite de CPU** como aqueles em sistemas de Machine Learning. +Mas você também pode explorar os benefícios do paralelismo e multiprocessamento (tendo múltiplos processadores rodando em paralelo) para trabalhos **limitados por CPU** como aqueles em sistemas de Machine Learning. -Isso, mais o simples fato que Python é a principal linguagem para **Data Science**, Machine Learning e especialmente Deep Learning, faz do FastAPI uma ótima escolha para APIs web e aplicações com Data Science / Machine Learning (entre muitas outras). +Isso, somado ao simples fato que Python é a principal linguagem para **Data Science**, Machine Learning e especialmente Deep Learning, faz do FastAPI uma ótima escolha para APIs web e aplicações com Data Science / Machine Learning (entre muitas outras). Para ver como alcançar esse paralelismo em produção veja a seção sobre [Deployment](deployment/index.md){.internal-link target=_blank}. ## `async` e `await` -Versões modernas do Python tem um modo muito intuitivo para definir código assíncrono. Isso faz parecer normal o código "sequencial" e fazer o "esperar" para você nos momentos certos. +Versões modernas do Python têm um modo muito intuitivo para definir código assíncrono. Isso faz parecer do mesmo jeito do código normal "sequencial" e fazer a "espera" para você nos momentos certos. -Quando tem uma operação que exigirá espera antes de dar os resultados e tem suporte para esses recursos Python, você pode escrever assim: +Quando tem uma operação que exigirá espera antes de dar os resultados e tem suporte para esses novos recursos do Python, você pode escrever assim: ```Python burgers = await get_burgers(2) ``` -A chave aqui é o `await`. Ele diz ao Python que ele tem que esperar por `get_burgers(2)` para finalizar suas coisas antes de armazenar os resultados em `burgers`. Com isso, o Python saberá que ele pode ir e fazer outras coisas nesse meio tempo (como receber outra requisição). +A chave aqui é o `await`. Ele diz ao Python que ele tem que esperar por `get_burgers(2)` finalizar suas coisas 🕙 antes de armazenar os resultados em `burgers`. Com isso, o Python saberá que ele pode ir e fazer outras coisas 🔀 ⏯ nesse meio tempo (como receber outra requisição). Para o `await` funcionar, tem que estar dentro de uma função que suporte essa assincronicidade. Para fazer isso, apenas declare a função com `async def`: ```Python hl_lines="1" async def get_burgers(number: int): - # Fazer alguma coisa assíncrona para criar os hambúrgueres + # Faz alguma coisa assíncrona para criar os hambúrgueres return burgers ``` @@ -295,9 +293,9 @@ def get_sequential_burgers(number: int): return burgers ``` -Com `async def`, o Python sabe que, dentro dessa função, tem que estar ciente das expressões `await`, e que isso pode "pausar" a execução dessa função, e poderá fazer outra coisa antes de voltar. +Com `async def`, o Python sabe que, dentro dessa função, ele deve estar ciente das expressões `await`, e que isso poderá "pausar" ⏸ a execução dessa função, e ir fazer outra coisa 🔀 antes de voltar. -Quando você quiser chamar uma função `async def`, você tem que "esperar". Então, isso não funcionará: +Quando você quiser chamar uma função `async def`, você tem que "esperar" ela. Então, isso não funcionará: ```Python # Isso não irá funcionar, porquê get_burgers foi definido com: async def @@ -319,13 +317,24 @@ async def read_burgers(): Você deve ter observado que `await` pode ser usado somente dentro de funções definidas com `async def`. -Mas ao mesmo tempo, funções definidas com `async def` tem que ser aguardadas. Então, funções com `async def` pdem ser chamadas somente dentro de funções definidas com `async def` também. +Mas ao mesmo tempo, funções definidas com `async def` têm que ser "aguardadas". Então, funções com `async def` pdem ser chamadas somente dentro de funções definidas com `async def` também. Então, sobre o ovo e a galinha, como você chama a primeira função async? Se você estivar trabalhando com **FastAPI** não terá que se preocupar com isso, porquê essa "primeira" função será a sua *função de operação de rota*, e o FastAPI saberá como fazer a coisa certa. -Mas se você quiser usar `async` / `await` sem FastAPI, verifique a documentação oficial Python. +Mas se você quiser usar `async` / `await` sem FastAPI, você também pode fazê-lo. + +### Escreva seu próprio código assíncrono + +Starlette (e **FastAPI**) são baseados no AnyIO, o que o torna compatível com ambos o asyncio da biblioteca padrão do Python, e o Trio. + +Em particular, você pode usar diretamente o AnyIO para seus casos de uso avançados de concorrência que requerem padrões mais avançados no seu próprio código. + +E até se você não estiver utilizando FastAPI, você também pode escrever suas próprias aplicações assíncronas com o AnyIO por ser altamente compatível e ganhar seus benefícios (e.g. *concorrência estruturada*). + +Eu criei outra biblioteca em cima do AnyIO, como uma fina camada acima, para melhorar um pouco as anotações de tipo e obter melhor **autocompletar**, **erros de linha**, etc. Ela também possui uma introdução amigável e um tutorial para ajudar você a **entender** e escrever **seu próprio código async**: Asyncer. Seria particularmente útil se você precisar **combinar código async com código regular** (bloqueador/síncrono). + ### Outras formas de código assíncrono @@ -337,25 +346,25 @@ Essa mesma sintaxe (ou quase a mesma) foi também incluída recentemente em vers Mas antes disso, controlar código assíncrono era bem mais complexo e difícil. -Nas versões anteriores do Python, você poderia utilizar threads ou Gevent. Mas o código é um pouco mais complexo de entender, debugar, e pensar sobre. +Nas versões anteriores do Python, você poderia utilizar threads ou Gevent. Mas o código é bem mais complexo de entender, debugar, e pensar sobre. -Nas versões anteriores do NodeJS / Navegador JavaScript, você poderia utilizar "callbacks". O que leva ao inferno do callback. +Nas versões anteriores do NodeJS / Navegador JavaScript, você utilizaria "callbacks". O que leva ao inferno do callback. ## Corrotinas -**Corrotina** é apenas um jeito bonitinho para a coisa que é retornada de uma função `async def`. O Python sabe que é uma função que pode começar e terminar em algum ponto, mas que pode ser pausada internamente também, sempre que tiver um `await` dentro dela. +**Corrotina** é apenas um jeito bonitinho para a coisa que é retornada de uma função `async def`. O Python sabe que é algo como uma função, que pode começar e que vai terminar em algum ponto, mas que pode ser pausada ⏸ internamente também, sempre que tiver um `await` dentro dela. -Mas toda essa funcionalidade de código assíncrono com `async` e `await` é muitas vezes resumida como "corrotina". É comparável ao principal recurso chave do Go, a "Gorotina". +Mas toda essa funcionalidade de código assíncrono com `async` e `await` é muitas vezes resumida como usando "corrotinas". É comparável ao principal recurso chave do Go, a "Gorrotina". ## Conclusão -Vamos ver a mesma frase com o conteúdo cima: +Vamos ver a mesma frase de cima: -> Versões modernas do Python tem suporte para **"código assíncrono"** usando algo chamado **"corrotinas"**, com sintaxe **`async` e `await`**. +> Versões modernas do Python têm suporte para **"código assíncrono"** usando algo chamado **"corrotinas"**, com sintaxe **`async` e `await`**. -Isso pode fazer mais sentido agora. +Isso pode fazer mais sentido agora. ✨ -Tudo isso é o que deixa o FastAPI poderoso (através do Starlette) e que o faz ter uma performance impressionante. +Tudo isso é o que empodera o FastAPI (através do Starlette) e que o faz ter uma performance tão impressionante. ## Detalhes muito técnicos @@ -365,25 +374,25 @@ Você pode provavelmente pular isso. Esses são detalhes muito técnicos de como **FastAPI** funciona por baixo do capô. -Se você tem algum conhecimento técnico (corrotinas, threads, blocking etc) e está curioso sobre como o FastAPI controla o `async def` vs normal `def`, vá em frente. +Se você tem certo conhecimento técnico (corrotinas, threads, blocking etc) e está curioso sobre como o FastAPI controla o `async def` vs normal `def`, vá em frente. /// ### Funções de operação de rota -Quando você declara uma *função de operação de rota* com `def` normal ao invés de `async def`, ela é rodada em uma threadpool externa que então é aguardada, ao invés de ser chamada diretamente (ela poderia bloquear o servidor). +Quando você declara uma *função de operação de rota* com `def` normal ao invés de `async def`, ela é rodada em uma threadpool externa que é então aguardada, ao invés de ser chamada diretamente (já que ela bloquearia o servidor). -Se você está chegando de outro framework assíncrono que não faz o trabalho descrito acima e você está acostumado a definir triviais *funções de operação de rota* com simples `def` para ter um mínimo ganho de performance (cerca de 100 nanosegundos), por favor observe que no **FastAPI** o efeito pode ser bem o oposto. Nesses casos, é melhor usar `async def` a menos que suas *funções de operação de rota* utilizem código que performem bloqueamento IO. +Se você está chegando de outro framework assíncrono que não funciona como descrito acima e você está acostumado a definir *funções de operação de rota* triviais somente de computação com simples `def` para ter um mínimo ganho de performance (cerca de 100 nanosegundos), por favor observe que no **FastAPI** o efeito pode ser bem o oposto. Nesses casos, é melhor usar `async def` a menos que suas *funções de operação de rota* utilizem código que performe bloqueamento IO. -Ainda, em ambas as situações, as chances são que o **FastAPI** será [ainda mais rápido](index.md#performance){.internal-link target=_blank} do que (ou ao menos comparável a) seus frameworks antecessores. +Ainda, em ambas as situações, as chances são que o **FastAPI** [ainda será mais rápido](index.md#performance){.internal-link target=_blank} do que (ou ao menos comparável a) seu framework anterior. ### Dependências -O mesmo se aplica para as dependências. Se uma dependência tem as funções com padrão `def` ao invés de `async def`, ela é rodada no threadpool externo. +O mesmo se aplica para as [dependências](tutorial/dependencies/index.md){.internal-link target=_blank}. Se uma dependência tem as funções com padrão `def` ao invés de `async def`, ela é rodada no threadpool externo. ### Sub-dependências -Você pode ter múltiplas dependências e sub-dependências exigindo uma a outra (como parâmetros de definições de funções), algumas delas podem ser criadas com `async def` e algumas com `def` normal. Isso ainda poderia funcionar, e aquelas criadas com `def` podem ser chamadas em uma thread externa ao invés de serem "aguardadas". +Você pode ter múltiplas dependências e [sub-dependências](tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requisitando uma à outra (como parâmetros de definições de funções), algumas delas podem ser criadas com `async def` e algumas com `def` normal. Isso ainda funcionaria, e aquelas criadas com `def` normal seriam chamadas em uma thread externa (do threadpool) ao invés de serem "aguardadas". ### Outras funções de utilidade @@ -395,6 +404,6 @@ Se sua função de utilidade é uma função normal com `def`, ela será chamada --- -Novamente, esses são detalhes muito técnicos que provavelmente possam ser úteis caso você esteja procurando por eles. +Novamente, esses são detalhes muito técnicos que provavelmente seriam úteis caso você esteja procurando por eles. Caso contrário, você deve ficar bem com as dicas da seção acima: Com pressa?. From 89b4d1f076be7f7b30447344ef33cc895dade875 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Jul 2025 11:17:02 +0000 Subject: [PATCH 18/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d5730e247..a6a68584a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Translations +* 🌐 Update Portuguese Translation for `docs/pt/docs/async.md`. PR [#13863](https://github.com/fastapi/fastapi/pull/13863) by [@EdmilsonRodrigues](https://github.com/EdmilsonRodrigues). * 📝 Fix highlight line in `docs/ja/docs/tutorial/body.md`. PR [#13927](https://github.com/fastapi/fastapi/pull/13927) by [@KoyoMiyazaki](https://github.com/KoyoMiyazaki). * 🌐 Add Persian translation for `docs/fa/docs/environment-variables.md`. PR [#13923](https://github.com/fastapi/fastapi/pull/13923) by [@Mohammad222PR](https://github.com/Mohammad222PR). * 🌐 Add Persian translation for `docs/fa/docs/python-types.md`. PR [#13524](https://github.com/fastapi/fastapi/pull/13524) by [@Mohammad222PR](https://github.com/Mohammad222PR). From 3d60b5e23e878ca28e823fa65b06d67de95a6588 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Sat, 26 Jul 2025 13:35:42 +0200 Subject: [PATCH 19/30] =?UTF-8?q?=F0=9F=93=9D=20Add=20discussion=20templat?= =?UTF-8?q?e=20for=20new=20language=20translation=20requests=20(#13535)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/DISCUSSION_TEMPLATE/translations.yml | 45 ++++++++++++++++++++ docs/en/docs/contributing.md | 37 +++++++++++----- 2 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 .github/DISCUSSION_TEMPLATE/translations.yml diff --git a/.github/DISCUSSION_TEMPLATE/translations.yml b/.github/DISCUSSION_TEMPLATE/translations.yml new file mode 100644 index 000000000..16e304d99 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/translations.yml @@ -0,0 +1,45 @@ +labels: [lang-all] +body: + - type: markdown + attributes: + value: | + Thanks for your interest in helping translate the FastAPI docs! 🌍 + + Please follow these instructions carefully to propose a new language translation. 🙏 + + This structured process helps ensure translations can be properly maintained long-term. + - type: checkboxes + id: checks + attributes: + label: Initial Checks + description: Please confirm and check all the following options. + options: + - label: I checked that this language is not already being translated in FastAPI docs. + required: true + - label: I searched existing discussions to ensure no one else proposed this language. + required: true + - label: I am a native speaker of the language I want to help translate. + required: true + - type: input + id: language + attributes: + label: Target Language + description: What language do you want to translate the FastAPI docs into? + placeholder: e.g. Latin + validations: + required: true + - type: textarea + id: additional_info + attributes: + label: Additional Information + description: Any other relevant information about your translation proposal + - type: markdown + attributes: + value: | + Translations are automatized with AI and then reviewed by native speakers. 🤖 🙋 + + This allows us to keep them consistent and up-to-date. + + If there are several native speakers commenting on this discussion and + committing to help review new translations, the FastAPI team will review it + and potentially make it an official translation. 😎 diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 91c39c927..2583fd1fb 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -315,30 +315,47 @@ Now you can translate it all and see how it looks as you save the file. Some of these files are updated very frequently and a translation would always be behind, or they include the main content from English source files, etc. +#### Request a New Language + +Let's say that you want to request translations for a language that is not yet translated, not even some pages. For example, Latin. + +If there is no discussion for that language, you can start by requesting the new language. For that, you can follow these steps: + +* Create a new discussion following the template. +* Get a few native speakers to comment on the discussion and commit to help review translations for that language. + +Once there are several people in the discussion, the FastAPI team can evaluate it and can make it an official translation. + +Then the docs will be automatically translated using AI, and the team of native speakers can review the translation, and help tweak the AI prompts. + +Once there's a new translation, for example if docs are updated or there's a new section, there will be a comment in the same discussion with the link to the new translation to review. + #### New Language -Let's say that you want to add translations for a language that is not yet translated, not even some pages. +/// note -Let's say you want to add translations for Creole, and it's not yet there in the docs. +These steps will be performed by the FastAPI team. + +/// -Checking the link from above, the code for "Creole" is `ht`. +Checking the link from above (List of ISO 639-1 codes), you can see that the 2-letter code for Latin is `la`. -The next step is to run the script to generate a new translation directory: +Now you can create a new directory for the new language, running the following script: ```console // Use the command new-lang, pass the language code as a CLI argument -$ python ./scripts/docs.py new-lang ht +$ python ./scripts/docs.py new-lang la -Successfully initialized: docs/ht +Successfully initialized: docs/la ```-Now you can check in your code editor the newly created directory `docs/ht/`. +Now you can check in your code editor the newly created directory `docs/la/`. -That command created a file `docs/ht/mkdocs.yml` with a simple config that inherits everything from the `en` version: +That command created a file `docs/la/mkdocs.yml` with a simple config that inherits everything from the `en` version: ```yaml INHERIT: ../en/mkdocs.yml @@ -350,11 +367,11 @@ You could also simply create that file with those contents manually. /// -That command also created a dummy file `docs/ht/index.md` for the main page, you can start by translating that one. +That command also created a dummy file `docs/la/index.md` for the main page, you can start by translating that one. You can continue with the previous instructions for an "Existing Language" for that process. -You can make the first pull request with those two files, `docs/ht/mkdocs.yml` and `docs/ht/index.md`. 🎉 +You can make the first pull request with those two files, `docs/la/mkdocs.yml` and `docs/la/index.md`. 🎉 #### Preview the result From d67f092db84c7c5cb7b882aa1e25c8a10b6727b6 Mon Sep 17 00:00:00 2001 From: github-actionsDate: Sat, 26 Jul 2025 11:36:02 +0000 Subject: [PATCH 20/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a6a68584a..0e5fa6303 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Add discussion template for new language translation requests. PR [#13535](https://github.com/fastapi/fastapi/pull/13535) by [@alejsdev](https://github.com/alejsdev). + ### Translations * 🌐 Update Portuguese Translation for `docs/pt/docs/async.md`. PR [#13863](https://github.com/fastapi/fastapi/pull/13863) by [@EdmilsonRodrigues](https://github.com/EdmilsonRodrigues). From ae02be90b6857cac1a00ea47c154457effcdcb5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Jul 2025 20:57:50 +0200 Subject: [PATCH 21/30] =?UTF-8?q?=F0=9F=94=A8=20Update=20translations=20sc?= =?UTF-8?q?ript=20to=20remove=20old=20(removed)=20files=20(#13928)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/es/llm-prompt.md | 50 ----------------------- scripts/translate.py | 93 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 86 insertions(+), 57 deletions(-) diff --git a/docs/es/llm-prompt.md b/docs/es/llm-prompt.md index 3340dbc99..936ed9bba 100644 --- a/docs/es/llm-prompt.md +++ b/docs/es/llm-prompt.md @@ -4,56 +4,6 @@ Use the informal grammar (use "tú" instead of "usted"). For instructions or titles in imperative, keep them in imperative, for example "Edit it" to "Edítalo". -There are special blocks of notes, tips and others that look like: - -/// note - -To translate it, keep the same line and add the translation after a vertical bar: - -/// note | Nota - -Some examples: - -Source: - -/// tip - -Result: - -/// tip | Consejo - -Source: - -/// details | Preview - -Result: - -/// details | Vista previa - -Source: - -/// warning - -Result: - -/// warning | Advertencia - -Source: - -/// info - -Result: - -/// info | Información - -Source: - -/// note | Technical Details - -Result: - -/// note | Detalles Técnicos - --- For the next terms, use the following translations: diff --git a/scripts/translate.py b/scripts/translate.py index 9a2136d1b..09533747b 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -5,6 +5,7 @@ from typing import Iterable import typer import yaml from pydantic_ai import Agent +from rich import print non_translated_sections = ( "reference/", @@ -28,8 +29,38 @@ The content is written in markdown, write the translation in markdown as well. D When there's an example of code, the console or a terminal, normally surrounded by triple backticks and a keyword like "console" or "bash" (e.g. ```console), do not translate the content, keep the original in English. The original content will be surrounded by triple percentage signs (%) and you should translate it to the target language. Do not include the triple percentage signs in the translation. + +There are special blocks of notes, tips and others that look like: + +/// note + +To translate it, keep the same line and add the translation after a vertical bar. + +For example, if you were translating to Spanish, you would write: + +/// note | Nota + +Some examples in Spanish: + +Source: + +/// tip + +Result: + +/// tip | Consejo + +Source: + +/// details | Preview + +Result: + +/// details | Vista previa """ +app = typer.Typer() + @lru_cache def get_langs() -> dict[str, str]: @@ -46,6 +77,17 @@ def generate_lang_path(*, lang: str, path: Path) -> Path: return out_path +def generate_en_path(*, lang: str, path: Path) -> Path: + en_docs_path = Path("docs/en/docs") + assert not str(path).startswith(str(en_docs_path)), ( + f"Path must not be inside {en_docs_path}" + ) + lang_docs_path = Path(f"docs/{lang}/docs") + out_path = Path(str(path).replace(str(lang_docs_path), str(en_docs_path))) + return out_path + + +@app.command() def translate_page(*, lang: str, path: Path) -> None: langs = get_langs() language = langs[lang] @@ -68,8 +110,8 @@ def translate_page(*, lang: str, path: Path) -> None: agent = Agent("openai:gpt-4o") prompt_segments = [ - lang_prompt_content, general_prompt, + lang_prompt_content, ] if old_translation: prompt_segments.extend( @@ -119,6 +161,7 @@ def iter_paths_to_translate() -> Iterable[Path]: yield path +@app.command() def translate_all(lang: str) -> None: paths_to_process: list[Path] = [] for path in iter_paths_to_translate(): @@ -151,12 +194,48 @@ def translate_all(lang: str) -> None: print(f"Done translating: {p}") -def main(*, lang: str, path: Path = None) -> None: - if path: - translate_page(lang=lang, path=path) - else: - translate_all(lang=lang) +@app.command() +def list_removable(lang: str) -> list[Path]: + removable_paths: list[Path] = [] + lang_paths = Path(f"docs/{lang}").rglob("*.md") + for path in lang_paths: + en_path = generate_en_path(lang=lang, path=path) + if not en_path.exists(): + removable_paths.append(path) + print(removable_paths) + return removable_paths + + +@app.command() +def list_all_removable() -> list[Path]: + all_removable_paths: list[Path] = [] + langs = get_langs() + for lang in langs: + if lang == "en": + continue + removable_paths = list_removable(lang) + all_removable_paths.extend(removable_paths) + print(all_removable_paths) + return all_removable_paths + + +@app.command() +def remove_removable(lang: str) -> None: + removable_paths = list_removable(lang) + for path in removable_paths: + path.unlink() + print(f"Removed: {path}") + print("Done removing all removable paths") + + +@app.command() +def remove_all_removable() -> None: + all_removable = list_all_removable() + for removable_path in all_removable: + removable_path.unlink() + print(f"Removed: {removable_path}") + print("Done removing all removable paths") if __name__ == "__main__": - typer.run(main) + app() From 4f0aae9d237059c48aace9cc222f62e3ef4f2ee5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Jul 2025 18:58:14 +0000 Subject: [PATCH 22/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0e5fa6303..3b94dfd8f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -23,6 +23,7 @@ hide: ### Internal +* 🔨 Update translations script to remove old (removed) files. PR [#13928](https://github.com/fastapi/fastapi/pull/13928) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#13894](https://github.com/fastapi/fastapi/pull/13894) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Update httpx requirement to >=0.23.0,<0.29.0. PR [#13114](https://github.com/fastapi/fastapi/pull/13114) by [@yan12125](https://github.com/yan12125). * 🔧 Update sponsors: Add Mobb. PR [#13916](https://github.com/fastapi/fastapi/pull/13916) by [@tiangolo](https://github.com/tiangolo). From adc328e258d00c5717070a103626cb48a03abecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Jul 2025 23:19:05 +0200 Subject: [PATCH 23/30] =?UTF-8?q?=F0=9F=94=A8=20Refactor=20translate=20scr?= =?UTF-8?q?ipt=20with=20extra=20feedback=20(prints)=20(#13932)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/translate.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/scripts/translate.py b/scripts/translate.py index 09533747b..6af12dc98 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -106,7 +106,9 @@ def translate_page(*, lang: str, path: Path) -> None: original_content = path.read_text() old_translation: str | None = None if out_path.exists(): + print(f"Found existing translation: {out_path}") old_translation = out_path.read_text() + print(f"Translating {path} to {lang} ({language})") agent = Agent("openai:gpt-4o") prompt_segments = [ @@ -131,13 +133,14 @@ def translate_page(*, lang: str, path: Path) -> None: ] ) prompt = "\n\n".join(prompt_segments) - + print(f"Running agent for {out_path}") result = agent.run_sync(prompt) out_content = f"{result.data.strip()}\n" + print(f"Saving translation to {out_path}") out_path.write_text(out_content) -def iter_paths_to_translate() -> Iterable[Path]: +def iter_all_en_paths() -> Iterable[Path]: """ Iterate on the markdown files to translate in order of priority. """ @@ -161,13 +164,16 @@ def iter_paths_to_translate() -> Iterable[Path]: yield path -@app.command() -def translate_all(lang: str) -> None: - paths_to_process: list[Path] = [] - for path in iter_paths_to_translate(): +def iter_en_paths_to_translate() -> Iterable[Path]: + for path in iter_all_en_paths(): if str(path).replace("docs/en/docs/", "").startswith(non_translated_sections): continue - paths_to_process.append(path) + yield path + + +@app.command() +def translate_all(lang: str) -> None: + paths_to_process = list(iter_en_paths_to_translate()) print("Original paths:") for p in paths_to_process: print(f" - {p}") From 4c40b484f1a6ecea0921dc58292c3f733ddf5d9b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Jul 2025 21:19:29 +0000 Subject: [PATCH 24/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3b94dfd8f..56c325977 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -23,6 +23,7 @@ hide: ### Internal +* 🔨 Refactor translate script with extra feedback (prints). PR [#13932](https://github.com/fastapi/fastapi/pull/13932) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update translations script to remove old (removed) files. PR [#13928](https://github.com/fastapi/fastapi/pull/13928) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#13894](https://github.com/fastapi/fastapi/pull/13894) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Update httpx requirement to >=0.23.0,<0.29.0. PR [#13114](https://github.com/fastapi/fastapi/pull/13114) by [@yan12125](https://github.com/yan12125). From 273b06adab5fd527492ff9ed3f98cfa653d10002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Jul 2025 23:27:35 +0200 Subject: [PATCH 25/30] =?UTF-8?q?=E2=9A=92=EF=B8=8F=20Update=20translate?= =?UTF-8?q?=20script,=20show=20and=20update=20outdated=20translations=20(#?= =?UTF-8?q?13933)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- requirements-translations.txt | 1 + scripts/translate.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/requirements-translations.txt b/requirements-translations.txt index 7a2a8004e..90f718032 100644 --- a/requirements-translations.txt +++ b/requirements-translations.txt @@ -1 +1,2 @@ pydantic-ai==0.0.30 +GitPython==3.1.45 diff --git a/scripts/translate.py b/scripts/translate.py index 6af12dc98..b78d7211b 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -2,6 +2,7 @@ from functools import lru_cache from pathlib import Path from typing import Iterable +import git import typer import yaml from pydantic_ai import Agent @@ -243,5 +244,39 @@ def remove_all_removable() -> None: print("Done removing all removable paths") +@app.command() +def list_outdated(lang: str) -> list[Path]: + dir_path = Path(__file__).absolute().parent.parent + repo = git.Repo(dir_path) + + outdated_paths: list[Path] = [] + en_lang_paths = list(iter_en_paths_to_translate()) + for path in en_lang_paths: + lang_path = generate_lang_path(lang=lang, path=path) + if not lang_path.exists(): + outdated_paths.append(path) + continue + en_commit_datetime = list(repo.iter_commits(paths=path, max_count=1))[ + 0 + ].committed_datetime + lang_commit_datetime = list(repo.iter_commits(paths=lang_path, max_count=1))[ + 0 + ].committed_datetime + if lang_commit_datetime < en_commit_datetime: + outdated_paths.append(path) + print(outdated_paths) + return outdated_paths + + +@app.command() +def update_outdated(lang: str) -> None: + outdated_paths = list_outdated(lang) + for path in outdated_paths: + print(f"Updating lang: {lang} path: {path}") + translate_page(lang=lang, path=path) + print(f"Done updating: {path}") + print("Done updating all outdated paths") + + if __name__ == "__main__": app() From d5d302c3da11262f4e320a9af5dbb9d13386e1b5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Jul 2025 21:27:58 +0000 Subject: [PATCH 26/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 56c325977..f3984fa9e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -23,6 +23,7 @@ hide: ### Internal +* ⚒️ Update translate script, show and update outdated translations. PR [#13933](https://github.com/fastapi/fastapi/pull/13933) by [@tiangolo](https://github.com/tiangolo). * 🔨 Refactor translate script with extra feedback (prints). PR [#13932](https://github.com/fastapi/fastapi/pull/13932) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update translations script to remove old (removed) files. PR [#13928](https://github.com/fastapi/fastapi/pull/13928) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#13894](https://github.com/fastapi/fastapi/pull/13894) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 6516a6c4a60c91de881cc73c40a3c609001dd29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Jul 2025 21:12:19 +0200 Subject: [PATCH 27/30] =?UTF-8?q?=F0=9F=91=B7=20Add=20CI=20to=20translate?= =?UTF-8?q?=20with=20LLMs=20(#13937)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/translate.yml | 77 ++++++++++++++++++++++++++ scripts/translate.py | 96 +++++++++++++++++++++++++++++---- 2 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/translate.yml diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml new file mode 100644 index 000000000..edbff6fe5 --- /dev/null +++ b/.github/workflows/translate.yml @@ -0,0 +1,77 @@ +name: Translate + +on: + workflow_dispatch: + inputs: + debug_enabled: + description: Run with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate) + required: false + default: "false" + command: + description: Command to run + type: choice + options: + - translate-page + - translate-lang + - update-outdated + - add-missing + - update-and-add + - remove-all-removable + lang: + description: Language to translate to as a letter code (e.g. "es" for Spanish) + type: string + required: false + default: "" + en_path: + description: File path in English to translate (e.g. docs/en/docs/index.md) + type: string + required: false + default: "" + +env: + UV_SYSTEM_PYTHON: 1 + +jobs: + job: + if: github.repository_owner == 'fastapi' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Setup uv + uses: astral-sh/setup-uv@v6 + with: + version: "0.4.15" + enable-cache: true + cache-dependency-glob: | + requirements**.txt + pyproject.toml + - name: Install Dependencies + run: uv pip install -r requirements-github-actions.txt -r requirements-translations.txt + # Allow debugging with tmate + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} + with: + limit-access-to-actor: true + env: + GITHUB_TOKEN: ${{ secrets.FASTAPI_TRANSLATIONS }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + - name: FastAPI Translate + run: | + python ./scripts/translate.py ${{ github.event.inputs.command }} + python ./scripts/translate.py make-pr + env: + GITHUB_TOKEN: ${{ secrets.FASTAPI_TRANSLATIONS }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + LANG: ${{ github.event.inputs.lang }} + EN_PATH: ${{ github.event.inputs.en_path }} diff --git a/scripts/translate.py b/scripts/translate.py index b78d7211b..5fdbe7220 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -1,10 +1,13 @@ +import secrets +import subprocess from functools import lru_cache from pathlib import Path -from typing import Iterable +from typing import Annotated, Iterable import git import typer import yaml +from github import Github from pydantic_ai import Agent from rich import print @@ -89,7 +92,11 @@ def generate_en_path(*, lang: str, path: Path) -> Path: @app.command() -def translate_page(*, lang: str, path: Path) -> None: +def translate_page( + *, + lang: Annotated[str, typer.Option(envvar="LANG")], + en_path: Annotated[Path, typer.Option(envvar="EN_PATH")], +) -> None: langs = get_langs() language = langs[lang] lang_path = Path(f"docs/{lang}") @@ -99,17 +106,17 @@ def translate_page(*, lang: str, path: Path) -> None: lang_prompt_content = lang_prompt_path.read_text() en_docs_path = Path("docs/en/docs") - assert str(path).startswith(str(en_docs_path)), ( + assert str(en_path).startswith(str(en_docs_path)), ( f"Path must be inside {en_docs_path}" ) - out_path = generate_lang_path(lang=lang, path=path) + out_path = generate_lang_path(lang=lang, path=en_path) out_path.parent.mkdir(parents=True, exist_ok=True) - original_content = path.read_text() + original_content = en_path.read_text() old_translation: str | None = None if out_path.exists(): print(f"Found existing translation: {out_path}") old_translation = out_path.read_text() - print(f"Translating {path} to {lang} ({language})") + print(f"Translating {en_path} to {lang} ({language})") agent = Agent("openai:gpt-4o") prompt_segments = [ @@ -173,7 +180,7 @@ def iter_en_paths_to_translate() -> Iterable[Path]: @app.command() -def translate_all(lang: str) -> None: +def translate_lang(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: paths_to_process = list(iter_en_paths_to_translate()) print("Original paths:") for p in paths_to_process: @@ -197,7 +204,7 @@ def translate_all(lang: str) -> None: print(f"Total paths to process: {len(missing_paths)}") for p in missing_paths: print(f"Translating: {p}") - translate_page(lang="es", path=p) + translate_page(lang="es", en_path=p) print(f"Done translating: {p}") @@ -244,6 +251,18 @@ def remove_all_removable() -> None: print("Done removing all removable paths") +@app.command() +def list_missing(lang: str) -> list[Path]: + missing_paths: list[Path] = [] + en_lang_paths = list(iter_en_paths_to_translate()) + for path in en_lang_paths: + lang_path = generate_lang_path(lang=lang, path=path) + if not lang_path.exists(): + missing_paths.append(path) + print(missing_paths) + return missing_paths + + @app.command() def list_outdated(lang: str) -> list[Path]: dir_path = Path(__file__).absolute().parent.parent @@ -254,7 +273,6 @@ def list_outdated(lang: str) -> list[Path]: for path in en_lang_paths: lang_path = generate_lang_path(lang=lang, path=path) if not lang_path.exists(): - outdated_paths.append(path) continue en_commit_datetime = list(repo.iter_commits(paths=path, max_count=1))[ 0 @@ -269,14 +287,70 @@ def list_outdated(lang: str) -> list[Path]: @app.command() -def update_outdated(lang: str) -> None: +def update_outdated(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: outdated_paths = list_outdated(lang) for path in outdated_paths: print(f"Updating lang: {lang} path: {path}") - translate_page(lang=lang, path=path) + translate_page(lang=lang, en_path=path) print(f"Done updating: {path}") print("Done updating all outdated paths") +@app.command() +def add_missing(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: + missing_paths = list_missing(lang) + for path in missing_paths: + print(f"Adding lang: {lang} path: {path}") + translate_page(lang=lang, en_path=path) + print(f"Done adding: {path}") + print("Done adding all missing paths") + + +@app.command() +def update_and_add(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: + print(f"Updating outdated translations for {lang}") + update_outdated(lang=lang) + print(f"Adding missing translations for {lang}") + add_missing(lang=lang) + print(f"Done updating and adding for {lang}") + + +@app.command() +def make_pr( + *, + lang: Annotated[str, typer.Option(envvar="LANG")], + github_token: Annotated[str, typer.Option(envvar="GITHUB_TOKEN")], + github_repository: Annotated[str, typer.Option(envvar="GITHUB_REPOSITORY")], +) -> None: + print("Setting up GitHub Actions git user") + repo = git.Repo(Path(__file__).absolute().parent.parent) + if not repo.is_dirty(untracked_files=True): + print("Repository is clean, no changes to commit") + return + subprocess.run(["git", "config", "user.name", "github-actions"], check=True) + subprocess.run( + ["git", "config", "user.email", "github-actions@github.com"], check=True + ) + branch_name = f"translate-{lang}-{secrets.token_hex(4)}" + print(f"Creating a new branch {branch_name}") + subprocess.run(["git", "checkout", "-b", branch_name], check=True) + print("Adding updated files") + lang_path = Path(f"docs/{lang}") + subprocess.run(["git", "add", str(lang_path)], check=True) + print("Committing updated file") + message = f"🌐 Update translations - {lang}" + subprocess.run(["git", "commit", "-m", message], check=True) + print("Pushing branch") + subprocess.run(["git", "push", "origin", branch_name], check=True) + print("Creating PR") + g = Github(github_token) + gh_repo = g.get_repo(github_repository) + pr = gh_repo.create_pull( + title=message, body=message, base="master", head=branch_name + ) + print(f"Created PR: {pr.number}") + print("Finished") + + if __name__ == "__main__": app() From c74e7d1ce45ddd39e5bf364aa20662855626de14 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Jul 2025 19:12:39 +0000 Subject: [PATCH 28/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f3984fa9e..f7fec547a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -23,6 +23,7 @@ hide: ### Internal +* 👷 Add CI to translate with LLMs. PR [#13937](https://github.com/fastapi/fastapi/pull/13937) by [@tiangolo](https://github.com/tiangolo). * ⚒️ Update translate script, show and update outdated translations. PR [#13933](https://github.com/fastapi/fastapi/pull/13933) by [@tiangolo](https://github.com/tiangolo). * 🔨 Refactor translate script with extra feedback (prints). PR [#13932](https://github.com/fastapi/fastapi/pull/13932) by [@tiangolo](https://github.com/tiangolo). * 🔨 Update translations script to remove old (removed) files. PR [#13928](https://github.com/fastapi/fastapi/pull/13928) by [@tiangolo](https://github.com/tiangolo). From 9aab3d9e2239c51628f580eff442b630bd165d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 28 Jul 2025 01:51:53 +0200 Subject: [PATCH 29/30] =?UTF-8?q?=E2=9A=92=EF=B8=8F=20Tweak=20translate=20?= =?UTF-8?q?script=20and=20CI=20(#13939)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/translate.yml | 4 +- scripts/translate.py | 79 ++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index edbff6fe5..fc6b4d730 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -17,7 +17,7 @@ on: - add-missing - update-and-add - remove-all-removable - lang: + language: description: Language to translate to as a letter code (e.g. "es" for Spanish) type: string required: false @@ -73,5 +73,5 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.FASTAPI_TRANSLATIONS }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - LANG: ${{ github.event.inputs.lang }} + LANGUAGE: ${{ github.event.inputs.language }} EN_PATH: ${{ github.event.inputs.en_path }} diff --git a/scripts/translate.py b/scripts/translate.py index 5fdbe7220..e18449685 100644 --- a/scripts/translate.py +++ b/scripts/translate.py @@ -94,12 +94,12 @@ def generate_en_path(*, lang: str, path: Path) -> Path: @app.command() def translate_page( *, - lang: Annotated[str, typer.Option(envvar="LANG")], + language: Annotated[str, typer.Option(envvar="LANGUAGE")], en_path: Annotated[Path, typer.Option(envvar="EN_PATH")], ) -> None: langs = get_langs() - language = langs[lang] - lang_path = Path(f"docs/{lang}") + language_name = langs[language] + lang_path = Path(f"docs/{language}") lang_path.mkdir(exist_ok=True) lang_prompt_path = lang_path / "llm-prompt.md" assert lang_prompt_path.exists(), f"Prompt file not found: {lang_prompt_path}" @@ -109,14 +109,14 @@ def translate_page( assert str(en_path).startswith(str(en_docs_path)), ( f"Path must be inside {en_docs_path}" ) - out_path = generate_lang_path(lang=lang, path=en_path) + out_path = generate_lang_path(lang=language, path=en_path) out_path.parent.mkdir(parents=True, exist_ok=True) original_content = en_path.read_text() old_translation: str | None = None if out_path.exists(): print(f"Found existing translation: {out_path}") old_translation = out_path.read_text() - print(f"Translating {en_path} to {lang} ({language})") + print(f"Translating {en_path} to {language} ({language_name})") agent = Agent("openai:gpt-4o") prompt_segments = [ @@ -135,7 +135,7 @@ def translate_page( ) prompt_segments.extend( [ - f"Translate to {language} ({lang}).", + f"Translate to {language} ({language_name}).", "Original content:", f"%%%\n{original_content}%%%", ] @@ -180,7 +180,7 @@ def iter_en_paths_to_translate() -> Iterable[Path]: @app.command() -def translate_lang(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: +def translate_lang(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: paths_to_process = list(iter_en_paths_to_translate()) print("Original paths:") for p in paths_to_process: @@ -189,7 +189,7 @@ def translate_lang(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: missing_paths: list[Path] = [] skipped_paths: list[Path] = [] for p in paths_to_process: - lang_path = generate_lang_path(lang=lang, path=p) + lang_path = generate_lang_path(lang=language, path=p) if lang_path.exists(): skipped_paths.append(p) continue @@ -204,16 +204,16 @@ def translate_lang(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: print(f"Total paths to process: {len(missing_paths)}") for p in missing_paths: print(f"Translating: {p}") - translate_page(lang="es", en_path=p) + translate_page(language="es", en_path=p) print(f"Done translating: {p}") @app.command() -def list_removable(lang: str) -> list[Path]: +def list_removable(language: str) -> list[Path]: removable_paths: list[Path] = [] - lang_paths = Path(f"docs/{lang}").rglob("*.md") + lang_paths = Path(f"docs/{language}").rglob("*.md") for path in lang_paths: - en_path = generate_en_path(lang=lang, path=path) + en_path = generate_en_path(lang=language, path=path) if not en_path.exists(): removable_paths.append(path) print(removable_paths) @@ -234,8 +234,8 @@ def list_all_removable() -> list[Path]: @app.command() -def remove_removable(lang: str) -> None: - removable_paths = list_removable(lang) +def remove_removable(language: str) -> None: + removable_paths = list_removable(language) for path in removable_paths: path.unlink() print(f"Removed: {path}") @@ -252,11 +252,11 @@ def remove_all_removable() -> None: @app.command() -def list_missing(lang: str) -> list[Path]: +def list_missing(language: str) -> list[Path]: missing_paths: list[Path] = [] en_lang_paths = list(iter_en_paths_to_translate()) for path in en_lang_paths: - lang_path = generate_lang_path(lang=lang, path=path) + lang_path = generate_lang_path(lang=language, path=path) if not lang_path.exists(): missing_paths.append(path) print(missing_paths) @@ -264,14 +264,14 @@ def list_missing(lang: str) -> list[Path]: @app.command() -def list_outdated(lang: str) -> list[Path]: +def list_outdated(language: str) -> list[Path]: dir_path = Path(__file__).absolute().parent.parent repo = git.Repo(dir_path) outdated_paths: list[Path] = [] en_lang_paths = list(iter_en_paths_to_translate()) for path in en_lang_paths: - lang_path = generate_lang_path(lang=lang, path=path) + lang_path = generate_lang_path(lang=language, path=path) if not lang_path.exists(): continue en_commit_datetime = list(repo.iter_commits(paths=path, max_count=1))[ @@ -287,38 +287,38 @@ def list_outdated(lang: str) -> list[Path]: @app.command() -def update_outdated(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: - outdated_paths = list_outdated(lang) +def update_outdated(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: + outdated_paths = list_outdated(language) for path in outdated_paths: - print(f"Updating lang: {lang} path: {path}") - translate_page(lang=lang, en_path=path) + print(f"Updating lang: {language} path: {path}") + translate_page(language=language, en_path=path) print(f"Done updating: {path}") print("Done updating all outdated paths") @app.command() -def add_missing(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: - missing_paths = list_missing(lang) +def add_missing(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: + missing_paths = list_missing(language) for path in missing_paths: - print(f"Adding lang: {lang} path: {path}") - translate_page(lang=lang, en_path=path) + print(f"Adding lang: {language} path: {path}") + translate_page(language=language, en_path=path) print(f"Done adding: {path}") print("Done adding all missing paths") @app.command() -def update_and_add(lang: Annotated[str, typer.Option(envvar="LANG")]) -> None: - print(f"Updating outdated translations for {lang}") - update_outdated(lang=lang) - print(f"Adding missing translations for {lang}") - add_missing(lang=lang) - print(f"Done updating and adding for {lang}") +def update_and_add(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None: + print(f"Updating outdated translations for {language}") + update_outdated(language=language) + print(f"Adding missing translations for {language}") + add_missing(language=language) + print(f"Done updating and adding for {language}") @app.command() def make_pr( *, - lang: Annotated[str, typer.Option(envvar="LANG")], + language: Annotated[str | None, typer.Option(envvar="LANGUAGE")] = None, github_token: Annotated[str, typer.Option(envvar="GITHUB_TOKEN")], github_repository: Annotated[str, typer.Option(envvar="GITHUB_REPOSITORY")], ) -> None: @@ -331,14 +331,19 @@ def make_pr( subprocess.run( ["git", "config", "user.email", "github-actions@github.com"], check=True ) - branch_name = f"translate-{lang}-{secrets.token_hex(4)}" + branch_name = "translate" + if language: + branch_name += f"-{language}" + branch_name += f"-{secrets.token_hex(4)}" print(f"Creating a new branch {branch_name}") subprocess.run(["git", "checkout", "-b", branch_name], check=True) print("Adding updated files") - lang_path = Path(f"docs/{lang}") - subprocess.run(["git", "add", str(lang_path)], check=True) + git_path = Path("docs") + subprocess.run(["git", "add", str(git_path)], check=True) print("Committing updated file") - message = f"🌐 Update translations - {lang}" + message = "🌐 Update translations" + if language: + message += f" for {language}" subprocess.run(["git", "commit", "-m", message], check=True) print("Pushing branch") subprocess.run(["git", "push", "origin", branch_name], check=True) From c6566716092b1d98eb5cb1b6827b98b3d378ed8a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Jul 2025 23:52:18 +0000 Subject: [PATCH 30/30] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f7fec547a..c7af5d6fa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -23,6 +23,7 @@ hide: ### Internal +* ⚒️ Tweak translate script and CI. PR [#13939](https://github.com/fastapi/fastapi/pull/13939) by [@tiangolo](https://github.com/tiangolo). * 👷 Add CI to translate with LLMs. PR [#13937](https://github.com/fastapi/fastapi/pull/13937) by [@tiangolo](https://github.com/tiangolo). * ⚒️ Update translate script, show and update outdated translations. PR [#13933](https://github.com/fastapi/fastapi/pull/13933) by [@tiangolo](https://github.com/tiangolo). * 🔨 Refactor translate script with extra feedback (prints). PR [#13932](https://github.com/fastapi/fastapi/pull/13932) by [@tiangolo](https://github.com/tiangolo).