From 21ae3aaf3d900947d540179fb1c74b39a4dd372b Mon Sep 17 00:00:00 2001 From: laclac102 Date: Wed, 17 Dec 2025 15:07:37 +1100 Subject: [PATCH 01/15] feat: update lint and test workflows --- .github/PULL_REQUEST_TEMPLATE.md | 28 +++++++++++++++ .github/workflows/lint.yml | 30 ++++++++++++++++ .github/workflows/test-coverage.yml | 55 +++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test-coverage.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a918073 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ + + +# Pull Request + +## Summary +[SBP-XXX](https://biocloud.atlassian.net/browse/SBP-XXX) + +## Changes +- +- + +## How to Test + + +## Type of change +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update + +## Checklist +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have added or updated documentation where necessary +- [ ] I have run linting and unit tests locally +- [ ] The code follows the project's style guidelines diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..49a978c --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,30 @@ +name: Lint + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + cache-dependency-path: requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install ruff + + - name: Run Ruff + run: ruff check app diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml new file mode 100644 index 0000000..2552fd6 --- /dev/null +++ b/.github/workflows/test-coverage.yml @@ -0,0 +1,55 @@ +name: Coverage + +on: + push: + branches: ["main", "test"] + pull_request: + branches: ["main", "test"] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: requirements.txt + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + python -m pip install pytest pytest-cov pytest-asyncio + + - name: Run tests with coverage + env: + ALLOWED_ORIGINS: http://localhost + SEQERA_API_URL: https://example.com/api + SEQERA_ACCESS_TOKEN: test-token + WORK_SPACE: demo-workspace + COMPUTE_ID: compute-123 + WORK_DIR: /tmp/work + run: | + pytest --cov=app --cov-report=xml --cov-report=term-missing --cov-fail-under=90 + + - name: Coverage summary + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + thresholds: "90 90" + output: console From a97be3fee10092788b016d3a6b221ab2e81489e8 Mon Sep 17 00:00:00 2001 From: laclac102 Date: Thu, 18 Dec 2025 13:48:15 +1100 Subject: [PATCH 02/15] feat: add tests --- .coverage | Bin 0 -> 69632 bytes .github/workflows/lint.yml | 14 +- .github/workflows/test-coverage.yml | 26 ++- .pre-commit-config.yaml | 32 +++ README.md | 59 ++++- pyproject.toml | 95 ++++++++ pytest.ini | 15 ++ requirements-dev.txt | 7 + tests/__init__.py | 1 + tests/conftest.py | 113 ++++++++++ tests/test_additional_coverage.py | 61 ++++++ tests/test_main.py | 86 ++++++++ tests/test_routes_workflows.py | 217 +++++++++++++++++++ tests/test_schemas.py | 261 ++++++++++++++++++++++ tests/test_services_datasets.py | 325 ++++++++++++++++++++++++++++ tests/test_services_seqera.py | 235 ++++++++++++++++++++ 16 files changed, 1536 insertions(+), 11 deletions(-) create mode 100644 .coverage create mode 100644 .pre-commit-config.yaml create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 requirements-dev.txt create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_additional_coverage.py create mode 100644 tests/test_main.py create mode 100644 tests/test_routes_workflows.py create mode 100644 tests/test_schemas.py create mode 100644 tests/test_services_datasets.py create mode 100644 tests/test_services_seqera.py diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..22c564d2b71b37cc68e9c4177e5ec2379f1cc6c5 GIT binary patch literal 69632 zcmeI4d3+V+)yJP_?%bI(bMKuYkVsf_AwUQugs_H1HlZxCgncJu0|MDd0)*X}iK15A zTKDy>R%vTni>+^MskPQxYkjL#Yr!gN)ryw3w6)f%HSc-ux#xndz90I}yZFx4T)xjN z_s;xg=69a+JSSPaaBf{&ZE{0XOG9N_a-`@Yq!d$wJ z3_JL4!d)if#zEn(ODu4U<2NS;#eN=dur7_=YF1j+xFURo3WN%T3WN&$|5m_T7&p83 z=pp^RZIxB^wQVhx)wM13o;YvOjHydzB$rH`Hg`snmL&({V z>gsEgb&WN3)s<~^jT@6~n`(LMZLPI6D$wCF40US5VX6HURn*nsUTw7-@gtgB>KZCr zb|$yf?kq}ekp8x|+V(bDfFDv@x3N)eksO@fy)fBQyP>wFwz0alm5yR?T}`1~80@#r z?%le{1GTAhs%)vo|6?CH8(eVuRAz6$52|izsZmE?)l%75y{WdfC|Q{~!|EnH&h|Ds z+m@z=lD4K~eO+VfG#gv%+UlAbleO)&)!W)?Yd-b_iV($Qp6Ty8 zL!Hw*f6Sig&C@$?z=LaSVp&h`sJOjmD;;q&f+_0|v&7?&G>i zfp=wXd+XME>~B?6Zfk2wwJY%ASCrygydw+D?s<9gz}D1_qU5Qls%vY_fc24^CbRex zZXLD$lkXe#lzj9iQBP5QeX?mo>XA`sv}y_ULTT5ItEK`-FVHf(FGR*y=nI*00}?X@kH8*7K9*OD##Pv1y*>y%nX z_ZfMgbn9epfr`=$+FlRC?B1)F^sUrwl)hT&f6R~ELVP7Z#zuKnLv3a2wiY&$|H>qJ zkJB#6&q#U=o=&l8bf4PmlTIhST}5dcz%zBTdrpq@=!sCiq<*3wISIAo^ds;Of#B>9 zzqzt)Q}UmWT`k4~FNhl5C**w6fvZEPC{^!9uv$wU@_`;H^&8Oi)lcLjhpd*IKIHRj zcNTB2tVcI#uB>apM;1MpB0y4YSW}4`S5`G`YfIg~s+qbyRqQ!^yz~L5+pIlqRhZfw zAMf&}*48=ntb{* zT6e0?*toqQbxa&wl+7=}hmM~wgocL7#+r1zQel`r6}?|Qoa%eXL~zGs0X~$i_=x_) zXYz-p*DA;2&mY!|ny620sH~yKU0swjK5@sU+JE{uibm5b+iKhD8fuejlhnbaZmb=Z zt;y<^T3m^-w6GxZU)=`kYE_h~cadJX}((FETsPwj_A4c+-idGEj4V8S@>8AFP zo8%u>p1xlepy6U~=gj*=X?KPks!89V%#E&K6{)~mrTaQ|VFHg*y6+48KYWD>gbIWT zgbIWTgbIWTgbIWTgbIWTgbIWTgbIB66o^PonkxU-+*gGA2Yd@(p#q@-p#q@-p#q@- zp#q@-p#q@-p#q@-p#q@-|2GwgYmq)Kmw1%AGSaJix(HzGuu;QGZ9FT@Jt5o^?qB|I zo?v*?p#q@-p#q@-p#q@-p#q@-p#q@-p#q@-p#q@-tUx@{NAf6uW=DEi=|KQB|8FJ! zD%>aBo83xxp!244kJIFgz(wIJR3KC!R3KC!R3KC!R3KC!R3KC!R3KE~|Gom_tcciI zva}U{!@gvD^QOw-Wo4sFTC1AXPjOXc^_JSknv%+9Y}dAIufsoSgRm9eu>PYw_=c;Lhg1x^qQEU1MEaMTOegINP05zh2*3vZJYG%ZB=<9qOCMMK&{UF-f@w$R64!y8DBe0i-_v8Ipl`Qx<)+35i%mSoXz%U ze21Fu0Ovypx#XNW~jHQ zdehZgM7?S1E&ccZCFq)P-*x}sEO3syN%wX4DffBzNA7prue*1;H@KI(f!ppjxK-{{ zcey*)9peslb6neb+v#-PbRKh_alYpqcAA~LotvGjoQs^@POY=b8RtxQhB^68u9I-I z#Jh>#Tl4K7#6A<-8<&Y?iPsWOCXOW@Nj#AFTH=nx^@)QCKd~cGpQucnpO}&uk{F%H z!@o_K9se-?=lIj{6Y&@0KaSrO|3>^s{HFM>_=WK+_p!6=sP#ANch)P`FRY(fJFM?oU$t(t zuCh9;I%|zJ#TsD^u+Fgn-h-2H9G-%Q;hS(5Tn`>>hfS~s%3(T`K@lV&8^HX)JY~LS z9y5PtK5X7^e%ZX$yxP3T++{YJRpv5tjycI3VGc5TnvSU%?-*|x$Bn0rM~&|oUo*a7 zTx(ov>@!-74aO>CzA?=hV+=9+8eI(|`d;++(O*ZOi#{HGFnTz8SMnTlK5;i}YQ3qh6&i)92`u^bz_X zy{GQznm$S&toPQt=z8Sc$jQjdk!K^1MUF)7jocZzK5|*)f=F9rO(ZW;9+@5~ixfq= zMNI8|?N#lU+IH=`+E=yPw5zn>fA1p_9@%LHcrQyu)0+Q2d2?LAiLF zdHxDHB*P2DpPA2pK%8QpbH4Zk^Xxg|_sp|qi{CNNoG<>E;aTD&^Yn+sTg>N87jH68 zJ5QWoo;ppu!8~QE_zm;qDdKhJNi)SO8J;X&W1jGsc$skN+* zFEWpQQasN*YP2}UJaUvc$~I9LMez)C zVS)G=^WZ}9AIt?q#giExES_KhP#S?XLcR&EoLW6+@E1re3Lohi2Imh32~U&&KJ*RI3~WqY}(@M%u!Q( zjaiS1uQF@8_==4q`xcJuYvLY-($b7h@nzoP9b4SX{CZ#UL*^Ir#beA*mWpSYA1f0t zFh4v=yvqE*Oz~Uh!}G5{ zN^h@^KQ8W0Z?B6(;!g3!OzUpd&b00lcZ)kRtuKnZ#1}Fx+)Mo1=~idExGU4T5kK?x zOzQ@5qqr^8x=!36Zq2j~ip#{!nbu|EI&sSxt%KsGOzX3_<&E6JZ~LscA+F9@;&r*C zQ(Vuh#Kqzgah+NvHBH|yE)dsdw(F=D*JQQ}M2Gl%rsa!3d@j@SgfFhnv@Q^yxQfNw zw_jYzymz0tf_cwgaXItuJ>nqquHE7?=AFC5XPMh~ic6Vyw2Moax9<=aGjH224luWE z6BjYJwuuXwTUtd2^VSv-FgI@%K66vE@R%E$#09B)LQ1`%aX({y!@dk&zn5`K{T{~a zx9m=RgeLVmaq%u*ys2&{5j?DdQ@XH_sBnYoT}#{9L6(`T$<96x?FN%(WB-ujv76e zv1HWwjHM-W7>Ae6W-J*#i?O(5CgYIe&oB-hGJ|o*(CLhYL(a?KsA-IY3a2s-95jXT z_`u1G`O_yc_Rl|;F|Yqb#@xIKjM=&48SU(Gj9`~B8ZeeIYK&pjqoXbK6`b4EwNboK z(?_a>_@CZ6BE34&S(;uJ=^UQkP@CFW!Z>N_FvbZdiWy5M6fq7j9m-hr+z`fL!wVUo zD;mr=bXWo7kfDPZ`wtn&SkQj}cxnECBxC>lJjP`Iz8QSE zPX?2{8DCEJV(fdOCu5(!Ju;ZgW$fLjJ7dq@=P;h=nZuYnDVs4Tw;N+tPFKcmSy_x- zyLDmA>gqDOSq@{uO)$n1aYj29W3+6W5iE<*0AP$7CZirT(pOm1qP$Sk_4LKoIwN3M z>i+1|Oa#1DI&J%??5fQcm-)vuLci8QAy}jOEV$ZTC+QaPucCH<_h4p9a z4eM9dlhzNdf49DZdGP0~1J*99-dbmsThpvjR-x5bzsHzv+-aON=bFvtBQOBgTixIT z%Y;+#8XScu;33SA55diFC3HYL)WZ^(1ruR7`IPy#dAs>}^B(hJ^Mtv_e8K#- z_Di!y`>DCY9A!>1dz)QMUHiUvpE=lg*Lc}DVmxa+X54FBZ>%vcGcGXNj7>(lQDzhw zNh8~U=m*hL(bu9!qfbO1iheVCW%N+==4eN>J-R+xA6*ii6&2Bm(Rg%tbU-v$e?fm+ zKcPRRKcatIzg_>FexbfYuhUoS^YqDjsh+RrM1HHAk-tS=j650nLFB&3b&)SdE{W`p z6hs;#qaziOg^}~L4{LcA>UYYtSmSrS9AA2^?Yh zw)=>CyL++wd3TRn0Iwz=3L;kIcuD9 zrwqqcikxgG=|JLx#3>v}IhuGP@lfKMi9?B-6IUiW674vSvOX~@u_Q4uF+4FKAriTX zc>L}7Q#g+DLi~|UmV{PuZ}N^&x%io7svDBk$6_@FR?dp)Z|#~ z-(o+FeJ}R)*yh-Uu^l*mvcQzw$+e<}G*B6@6;m9R!nji*)lE3U1!Vq zFyPX zFCCCyO?R3AdyZd{_heSxaX@}0v+7Q9hx~G;b%(f9eks$s3D+IUv~CeMN$#qGxLw>L zxswUvHgUV;jw^^;#ch&1t{`p}w@U7~g1AZCEV<(f*crV^a>o_0GkT-suBvL?ExD@- z*crV+a#t0^wc6IV;_xPrJ^Tr0V&3gV#noaC-5h%3ZFc|+!mE*Dow?yIUT zFPGP4)?F$tme*!lmxxOx_f`RWwFf2lRzVyPmq_ldg1AT=klb4Z?A2Z*ugZK1_HyNw znHKhP`ofxE1*cfaH=D;(G>xyuSWJ0*8nq3MX^E-N%PN$#@3<|C53tgvOX zyFqf76{>6H1?gKJs;cFF=E^F$ zk9mEi+?(M>xrce(dP)6OsR53vXXi1M7f@M+~cx> zxon(V$2_J?u4R6Fj9kM!W~^MzJiJt{VlEjjS27ou$Q8^ZkICiC#m~rP87`JfnV%_^ zOPGs_{RhaI%*p=pGt7OHat3qWt8zMX-#mF9^Q(R3H0C}9aw>DrlX434IX&fM z=A3inB<7Pj@?7TZK5`;+R<@kL>}JXF%n4VHV~!J>s7>vwLNxfi8VNu$y3~^ECK3R}yU5Lgu zDAVc?7s`Q|ma@44nU=D-{B-Owgx@I#r@!CWf069RjFy*V-g8j)W!}9<_F>+&TlQvd z-z9r8?`W4jn78kcxy)_bW%snd8m;KBy?8}yn>>fP`H;+JZfcg@n9)yw;UmfPU%`(BfsZPe3H*S(KX7pE^8U58_uBny4TwNngX7pNv8ND{jj9#l_ zk7UQ`G4B61N?%kAVxJF#7^~|ig>ZsNuv+8Pbm6n@nT_vv8x@TIK;JR}% zt%{ilW zjh4u?E))l}c&3Fnw-(Eskhf2>GplegO0&|pn1#bz8gOHu=+OAuS*`muzIN8GZ5m%Y zYv(RaPhUrCN4usmZ{MLw=54K-V8$D|lNs;f51AWV<=>f`8|4Sgct5|-jQ8_<%yo_O zZ_Hcjj595#m@(%6ff;97 ze$Tvmjr<++s@3u&GsgelGGqLIi+TA9`6lzSsQeZ4@R9O)W{kJT zn1_|fqs+y_W>in(8Z`DBI% z%AYY~{QM8*SCaAx=Dfl3r_4R_{2_DBN%=5yc8>f3^T}-a5ObGo`F-ZBQ}RLPE?M$>%%{4@Bg}~|^1I9!SHFYx z|8lB!xtjk!g|+`Ly3e|gyAQedW9Iq=_j;WB?{Ifv9=pL^<1TV%xsxzc9fCCi=eTiK zbKZ4+@4V(b?>yx^ika$R=a6%&^Lgh|%vHBL^-h(u!kLfR>Ud|kQ{ePRc?U=V#B~~QnC#EOHW9B+2(L2#K0ayd@cKogQ%kk&p zPsAU_-1TemJL5NE1;9m^ySBtP#n;7`#OK7P#K**oup%Hg?!@)jd$Cio-^5;sJso>2 z_8?XQd^vV|?Aq96F+bKGYmC*zR>jI=Gq4(^JNe?4R3@+TXSB zwZCZJXkURNAv^6xyV_o1pKnjHOKBv;>co+d-&n`2C#>&VhpjJK*IS>p_E}r48f&>V z$2u3Q0tQ=st!&GJPIw#M#L9qY;4%0f9EQ7bM}%_(uXX z^RMPf^A+qu-6* z8~q|y3S5DsAv>du(dy`m=)CB(=-6mcv>(3swsp z(D&$D^$j>GQm)U?$LYgxTqIXd=vw5jk+*PUu)~PoyQXDY7oI zI5ImjDKauLIMOH56)Ock(Eh0XM*Efal=dU-JKEQ^JGC3|7Q`3$Q=>RiDWOjR_)`db zo&-7aa70Uj27U=G)O6DyrV#G~(=Se~Hj#=_ z%gj#jho&|(Rg7b+yuT&xM3ss7GdoM z@PV+V7JQSix(0lMu&NsTC}Cw4_?SQ_{nl54A0ez*557jYt^#~XxON@*f^f}R@b(j~ zUIX4f!d0um+e^4|6?l6HSF8kYH{qcb;O!z@dKkQ&giDr!*G{;233xjQ7cB;FJK@4b z;B6x;UkF|s;evATS_$Vb0I!Ac@O<#L63#?yCOrQE@R|tcoDW_j;p{o!H4x644PHIr z%vs=VA$(vac$*2QKMYlJO#WO!by|Ct0sJS z5_naFWr&sO4e;C66Ha&xyb8kc6Tn+XIBq<6YYEH7fwzY6u`=*h6P6;bA{_lBcq<7< zjRtQ8;mA?oEhii?61-)Ec0PDZ2}?(Sw}kM?Qt%cN4t){4MTEse!COdJR198udV?bH z77!NT2J;CE3&5L4IJgkJx#X?*$$?v60&yJaS^A zdoFn7#6~tQB_}p=vcV%KHnMZTBPTY{pFMJ7qiYs;dBDJzy%v=cmK#5l(`{#YM#{z}BZI5hN$NGf3Y){!R)&Zy@2isFdtQuIKV0+4nRRc$WZBLo8YT%Hs?I}A} z4J=2nJ!QzMft3ojr!1M)9JD><$*O^s3AU$P85^oF!Sa+Z<2*UmCD@)aX0;Wjf|i%E z=2TUJnY6xh7kFgSdizfB$fR`)Dju1%jzPsElh(Iw2ailz zZ`%eQnY51a#Uqo}TUx;*lh(JkfJY{+H*W=xOj^fK`07;4C)vHAeWqIC>3r_3`6@{8vY>Ci~SV9tUz?eab{2^VKmdkoW2s707z^ zb0+~guRifyAmi1?jsx;tz3e!U?doM^K(4EgEdw%LebiVW&(%jw1hQOx^e7<5)iD5& z;p!!$fc#c3EdjDyeRwI5+v+95fy`DfE&=jdeMm8o)#^is06DEbWGIl)>V-pqd{!?U z1!S}Oph6&*)dvm&GFkojKp>CR^QQw@tlmE#$YJ%o{y+w+=jH+VtDc<;WUsoN4dkvm z*g)p0U0|tU}P9zM1$~KWuC*_$)$^M`WQ_;(y+)_~z zlvN_VoCM{QNPSO$GD)O9eL*>-q9iDPM8YtbGDoTP>R9M9Rqp zWr;{xIiUOysaqD18ERd-0ePW@fsU+D!|a5dP{ZVej8MaPMn0%vI3pX>Ff$<+)G#q2 z6V&jEk_T#dMacp+yrRkhRe2eZ|Dh$DDf_duPD6{T3V=>S;=j`BRKNfKyfICspeeNOm7WZoR0M1Qs#oT|jTkg(q$Gau&0Jn$hxDm|!f9L$#`K9xO^8@F8 z=PSx{sxzqga+7>W0Bp89pn`JepQivMM)n*Y&Q^`Do> zPFSg$e^v4SG}indiQgN)BYtiC63q3R;#KjbshR3w@qY2_xQSW*@37YYnb@PT2V(cc zZi`)wGu7>}EwOd6g|X?eve?jAUMxFi*&o_}w%@dm+t1oRu^+VW!zzbc?5phq_8zSF z-(at{%k3HVc)J9%`yRGqN33_T;{VsyFEPLWfptGtI^1quV_jESljSAJP%L8Bk%xx4er4De@vQS2Ua&!!ZJ7?rodODs8zI!Ho}?>5VR6v z#uBuo5tnWytXK*`Ga)7@K@%ZnC_y734tWI)ggE3C)DvQo5^N#FA+KOFAr5&3b%e`T zK(L7rlb2v4;nHOgY)Io02x9;S0GqKnBO0Qg@glfX?c2sd59t5)qd!K+{79kGm1TzVH_JZIugt-S@))ZNA|*0b)+q=mld-yFK|raDb*c*l6XP)gTy23$X_rj8KRruMm`` z7GeP*1j7k&1TQEd9C8eTVT6T4te_ZCld5>o4vG|OsoFs+7@A&j2d)^BUV)W^c2JmZ zVBw%03{E$&ZqN=2(hV#dw1YwE238H)!N7C_iw5mrK)Qi7gLaUgZeYov9jMet9ROAg z+JVY`R09hJ?LZ|!s-Z@U0+j<%<8C`pX^?8Dp`$=$LaKqqf_9*iA=SWIK|4s9I5x0U z&<@DOQg>yq>-GH{~lXU~yrcc%lOBaJr)(tp4;FER3V%&|a8x}1ApR5}e z%mbgS8*n1PC+miJi@+!AhPm^>C+mhebHOL;1{~+{$+`gtdVI2OI5G=-vTm5Z6MV96 zICnbuWZf|J5%9^n0mu1$vTncu9iOZla74!^>xPMwz$fd5M<#+#)(tqkjoUZ@yWVjWftQ*kVeX?#SECQdb8;%u%Pu30juYyn34LB^}lXXKsTuRmr z{ayl}tQ-3E1D~uL3h>a$y5S{UO4bcYM6zxefS*9t4gCjzPu2}M-r=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "sbp-backend" +version = "1.0.0" +description = "Structural Biology Platform Backend API" +requires-python = ">=3.10" + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--strict-markers", + "--strict-config", + "--showlocals", +] + +[tool.coverage.run] +source = ["app"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/.venv/*", +] +branch = true + +[tool.coverage.report] +precision = 2 +show_missing = true +skip_covered = false +fail_under = 90 +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "@abstractmethod", +] + +[tool.coverage.html] +directory = "htmlcov" + +# Ruff configuration +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long (handled by black) + "B008", # do not perform function calls in argument defaults +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # imported but unused +"tests/*" = ["B018"] # useless expression + +# Black configuration +[tool.black] +line-length = 100 +target-version = ["py310", "py311", "py312"] +include = '\.pyi?$' +extend-exclude = ''' +/( + \.git + | \.venv + | __pycache__ + | build + | dist +)/ +''' + +# MyPy configuration +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = false +ignore_missing_imports = true diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5adbc4b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,15 @@ +[pytest] +asyncio_mode = auto +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +markers = + unit: Unit tests + integration: Integration tests + slow: Slow running tests +addopts = + --strict-markers + --strict-config + --showlocals + -v diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..9057fbb --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +# Development dependencies +pytest==8.0.0 +pytest-asyncio==0.23.5 +pytest-cov==4.1.0 +pytest-mock==3.12.0 +httpx==0.27.0 +coverage[toml]==7.4.1 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..55302ad --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for SBP Backend.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e492955 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,113 @@ +"""Shared test fixtures and configuration.""" +from __future__ import annotations + +import os +from typing import AsyncGenerator, Dict, Generator +from unittest.mock import AsyncMock, MagicMock + +import pytest +from fastapi.testclient import TestClient +from httpx import AsyncClient + +# Set test environment variables before importing app +os.environ["ALLOWED_ORIGINS"] = "http://localhost:3000,http://localhost:4200" +os.environ["SEQERA_API_URL"] = "https://api.seqera.test" +os.environ["SEQERA_ACCESS_TOKEN"] = "test_token_12345" +os.environ["WORK_SPACE"] = "test_workspace_id" +os.environ["COMPUTE_ID"] = "test_compute_env_id" +os.environ["WORK_DIR"] = "/test/work/dir" + +from app.main import create_app + + +@pytest.fixture +def app(): + """Create a FastAPI app instance for testing.""" + return create_app() + + +@pytest.fixture +def client(app) -> Generator[TestClient, None, None]: + """Create a test client for the FastAPI app.""" + with TestClient(app) as test_client: + yield test_client + + +@pytest.fixture +async def async_client(app) -> AsyncGenerator[AsyncClient, None]: + """Create an async test client for the FastAPI app.""" + async with AsyncClient(app=app, base_url="http://test") as ac: + yield ac + + +@pytest.fixture +def mock_httpx_response(): + """Create a mock httpx Response.""" + def _create_response( + status_code: int = 200, + json_data: Dict | None = None, + text: str = "", + is_error: bool = False, + ): + response = MagicMock() + response.status_code = status_code + response.is_error = is_error + response.text = text + if json_data: + response.json.return_value = json_data + return response + return _create_response + + +@pytest.fixture +def mock_async_client(mock_httpx_response): + """Create a mock async HTTP client.""" + mock_client = AsyncMock() + mock_client.post = AsyncMock() + mock_client.get = AsyncMock() + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock() + return mock_client + + +@pytest.fixture +def sample_workflow_launch_form(): + """Sample workflow launch form data.""" + return { + "pipeline": "https://github.com/nextflow-io/hello", + "revision": "main", + "configProfiles": ["singularity"], + "runName": "test-workflow-run", + "paramsText": "test_param: value", + } + + +@pytest.fixture +def sample_form_data(): + """Sample form data for dataset creation.""" + return { + "sample_name": "test_sample", + "input_file": "/path/to/file.txt", + "parameter1": "value1", + "parameter2": 42, + } + + +@pytest.fixture +def sample_seqera_dataset_response(): + """Sample Seqera dataset creation response.""" + return { + "id": "dataset_123abc", + "name": "test-dataset", + "description": "Test dataset", + "workspaceId": "test_workspace_id", + } + + +@pytest.fixture +def sample_seqera_launch_response(): + """Sample Seqera workflow launch response.""" + return { + "workflowId": "workflow_xyz789", + "status": "submitted", + } diff --git a/tests/test_additional_coverage.py b/tests/test_additional_coverage.py new file mode 100644 index 0000000..9da9885 --- /dev/null +++ b/tests/test_additional_coverage.py @@ -0,0 +1,61 @@ +"""Additional tests to increase coverage.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest + +from app.routes.workflows import get_details, upload_dataset +from app.schemas.workflows import DatasetUploadRequest +from app.services.datasets import DatasetUploadResult + + +class TestUploadDataset: + """Tests for dataset upload endpoint.""" + + @patch("app.routes.workflows.upload_dataset_to_seqera") + @patch("app.routes.workflows.create_seqera_dataset") + async def test_upload_dataset_success(self, mock_create, mock_upload): + """Test successful dataset upload.""" + # Mock dataset creation + mock_create_result = AsyncMock() + mock_create_result.dataset_id = "dataset_123" + mock_create_result.raw_response = {"id": "dataset_123"} + mock_create.return_value = mock_create_result + + # Mock upload + mock_upload_result = DatasetUploadResult( + success=True, + dataset_id="dataset_123", + message="Uploaded", + ) + mock_upload.return_value = mock_upload_result + + # Create request + request = DatasetUploadRequest( + formData={"sample": "test"}, + datasetName="test-dataset", + ) + + # Execute + response = await upload_dataset(request) + + # Verify + assert response.success is True + assert response.datasetId == "dataset_123" + mock_create.assert_called_once() + mock_upload.assert_called_once() + + +class TestGetDetails: + """Tests for get details endpoint.""" + + async def test_get_details_returns_placeholder(self): + """Test that get_details returns proper placeholder data.""" + result = await get_details("run_abc123") + + assert result.id == "run_abc123" + assert result.status == "pending" + assert result.runName == "N/A" + assert isinstance(result.configFiles, list) + assert isinstance(result.params, dict) diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..b8ce797 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,86 @@ +"""Tests for the main FastAPI application.""" +from __future__ import annotations + +import os +from unittest.mock import patch + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + + +def test_create_app_success(): + """Test that create_app creates a valid FastAPI instance.""" + from app.main import create_app + + app = create_app() + + assert isinstance(app, FastAPI) + assert app.title == "SBP Portal Backend" + assert app.version == "1.0.0" + + +def test_create_app_missing_allowed_origins(): + """Test that create_app raises error when ALLOWED_ORIGINS is missing.""" + from app.main import create_app + + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(RuntimeError, match="ALLOWED_ORIGINS environment variable is required"): + create_app() + + +def test_health_endpoint(client: TestClient): + """Test the /health endpoint returns correct response.""" + response = client.get("/health") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "ok" + assert "timestamp" in data + + +def test_cors_middleware_configured(app: FastAPI): + """Test that CORS middleware is properly configured.""" + # Check that middleware is added + middleware_found = False + for middleware in app.user_middleware: + if "CORSMiddleware" in str(middleware): + middleware_found = True + break + + assert middleware_found, "CORS middleware should be configured" + + +def test_workflow_router_included(app: FastAPI): + """Test that workflow router is included with correct prefix.""" + route_paths = [route.path for route in app.routes] + + assert "/api/workflows/launch" in route_paths + assert "/api/workflows/runs" in route_paths + + +def test_exception_handler(client: TestClient): + """Test that global exception handler works.""" + # Try to access a non-existent endpoint + response = client.get("/nonexistent") + + # Should return 404 but not crash + assert response.status_code == 404 + + +def test_cors_allowed_origins_parsing(): + """Test that ALLOWED_ORIGINS is correctly parsed from environment.""" + from app.main import create_app + + with patch.dict(os.environ, {"ALLOWED_ORIGINS": "http://localhost:3000, http://localhost:4200"}): + app = create_app() + assert app is not None + + +def test_cors_allowed_origins_with_empty_values(): + """Test that empty values in ALLOWED_ORIGINS are filtered out.""" + from app.main import create_app + + with patch.dict(os.environ, {"ALLOWED_ORIGINS": "http://localhost:3000,, , http://localhost:4200"}): + app = create_app() + assert app is not None diff --git a/tests/test_routes_workflows.py b/tests/test_routes_workflows.py new file mode 100644 index 0000000..13818d4 --- /dev/null +++ b/tests/test_routes_workflows.py @@ -0,0 +1,217 @@ +"""Tests for workflow routes.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest +from fastapi.testclient import TestClient + +from app.services.seqera import ( + SeqeraConfigurationError, + SeqeraLaunchResult, + SeqeraServiceError, +) + + +class TestLaunchWorkflow: + """Tests for POST /api/workflows/launch endpoint.""" + + @patch("app.routes.workflows.launch_seqera_workflow") + async def test_launch_success_without_dataset(self, mock_launch, client: TestClient): + """Test successful workflow launch without dataset.""" + mock_launch.return_value = SeqeraLaunchResult( + workflow_id="wf_123", + status="submitted", + message="Success", + ) + + payload = { + "launch": { + "pipeline": "https://github.com/test/repo", + "runName": "test-run", + } + } + + response = client.post("/api/workflows/launch", json=payload) + + assert response.status_code == 201 + data = response.json() + assert data["runId"] == "wf_123" + assert data["status"] == "submitted" + assert "submitTime" in data + + @patch("app.routes.workflows.upload_dataset_to_seqera") + @patch("app.routes.workflows.create_seqera_dataset") + @patch("app.routes.workflows.launch_seqera_workflow") + async def test_launch_success_with_form_data( + self, mock_launch, mock_create_dataset, mock_upload, client: TestClient + ): + """Test successful workflow launch with form data.""" + # Mock dataset creation + mock_create_result = AsyncMock() + mock_create_result.dataset_id = "dataset_456" + mock_create_dataset.return_value = mock_create_result + + # Mock dataset upload + mock_upload.return_value = None + + # Mock workflow launch + mock_launch.return_value = SeqeraLaunchResult( + workflow_id="wf_789", + status="submitted", + ) + + payload = { + "launch": { + "pipeline": "https://github.com/test/repo", + "runName": "test-with-data", + }, + "formData": { + "sample": "test", + "input": "/path/file.txt", + } + } + + response = client.post("/api/workflows/launch", json=payload) + + assert response.status_code == 201 + data = response.json() + assert data["runId"] == "wf_789" + + # Verify dataset creation was called + mock_create_dataset.assert_called_once() + mock_upload.assert_called_once() + + @patch("app.routes.workflows.launch_seqera_workflow") + async def test_launch_configuration_error(self, mock_launch, client: TestClient): + """Test launch with configuration error.""" + mock_launch.side_effect = SeqeraConfigurationError("Missing API token") + + payload = { + "launch": { + "pipeline": "https://github.com/test/repo", + } + } + + response = client.post("/api/workflows/launch", json=payload) + + assert response.status_code == 500 + assert "Missing API token" in response.json()["detail"] + + @patch("app.routes.workflows.launch_seqera_workflow") + async def test_launch_service_error(self, mock_launch, client: TestClient): + """Test launch with Seqera service error.""" + mock_launch.side_effect = SeqeraServiceError("API returned 502") + + payload = { + "launch": { + "pipeline": "https://github.com/test/repo", + } + } + + response = client.post("/api/workflows/launch", json=payload) + + assert response.status_code == 502 + assert "API returned 502" in response.json()["detail"] + + def test_launch_invalid_payload(self, client: TestClient): + """Test launch with invalid payload.""" + payload = { + "launch": { + "pipeline": "", # Empty pipeline + } + } + + response = client.post("/api/workflows/launch", json=payload) + + assert response.status_code == 422 # Validation error + + +class TestCancelWorkflow: + """Tests for POST /api/workflows/{run_id}/cancel endpoint.""" + + def test_cancel_workflow_success(self, client: TestClient): + """Test successful workflow cancellation.""" + response = client.post("/api/workflows/run_123/cancel") + + assert response.status_code == 200 + data = response.json() + assert data["runId"] == "run_123" + assert data["status"] == "cancelled" + assert "message" in data + + +class TestListRuns: + """Tests for GET /api/workflows/runs endpoint.""" + + def test_list_runs_default_params(self, client: TestClient): + """Test listing runs with default parameters.""" + response = client.get("/api/workflows/runs") + + assert response.status_code == 200 + data = response.json() + assert "runs" in data + assert data["limit"] == 50 + assert data["offset"] == 0 + assert data["total"] == 0 + + def test_list_runs_with_filters(self, client: TestClient): + """Test listing runs with filter parameters.""" + response = client.get( + "/api/workflows/runs", + params={ + "status": "running", + "workspace": "test_ws", + "limit": 10, + "offset": 5, + } + ) + + assert response.status_code == 200 + data = response.json() + assert data["limit"] == 10 + assert data["offset"] == 5 + + def test_list_runs_limit_validation(self, client: TestClient): + """Test that limit must be between 1 and 200.""" + # Test limit too high + response = client.get("/api/workflows/runs", params={"limit": 300}) + assert response.status_code == 422 + + # Test limit too low + response = client.get("/api/workflows/runs", params={"limit": 0}) + assert response.status_code == 422 + + def test_list_runs_offset_validation(self, client: TestClient): + """Test that offset must be non-negative.""" + response = client.get("/api/workflows/runs", params={"offset": -1}) + assert response.status_code == 422 + + +class TestGetLogs: + """Tests for GET /api/workflows/{run_id}/logs endpoint.""" + + def test_get_logs_success(self, client: TestClient): + """Test successful log retrieval.""" + response = client.get("/api/workflows/run_123/logs") + + assert response.status_code == 200 + data = response.json() + assert "entries" in data + assert "truncated" in data + assert "pending" in data + assert isinstance(data["entries"], list) + + +class TestGetDetails: + """Tests for GET /api/workflows/{run_id}/details endpoint.""" + + def test_get_details_success(self, client: TestClient): + """Test successful details retrieval.""" + response = client.get("/api/workflows/run_123/details") + + assert response.status_code == 200 + data = response.json() + assert data["id"] == "run_123" + assert "status" in data + assert "runName" in data diff --git a/tests/test_schemas.py b/tests/test_schemas.py new file mode 100644 index 0000000..0d1abbe --- /dev/null +++ b/tests/test_schemas.py @@ -0,0 +1,261 @@ +"""Tests for Pydantic schemas.""" +from __future__ import annotations + +from datetime import datetime + +import pytest +from pydantic import ValidationError + +from app.schemas.workflows import ( + CancelWorkflowResponse, + LaunchDetails, + LaunchLogs, + ListRunsResponse, + RunInfo, + WorkflowLaunchForm, + WorkflowLaunchPayload, + WorkflowLaunchResponse, +) + + +class TestWorkflowLaunchForm: + """Tests for WorkflowLaunchForm schema.""" + + def test_valid_minimal_form(self): + """Test WorkflowLaunchForm with minimal valid data.""" + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + assert form.pipeline == "https://github.com/test/repo" + assert form.revision is None + assert form.configProfiles == [] + assert form.runName is None + assert form.paramsText is None + + def test_valid_complete_form(self): + """Test WorkflowLaunchForm with all fields.""" + form = WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + revision="main", + configProfiles=["docker", "test"], + runName="my-test-run", + paramsText="param1: value1\nparam2: value2", + ) + + assert form.pipeline == "https://github.com/test/repo" + assert form.revision == "main" + assert form.configProfiles == ["docker", "test"] + assert form.runName == "my-test-run" + assert "param1" in form.paramsText + + def test_pipeline_required(self): + """Test that pipeline field is required.""" + with pytest.raises(ValidationError) as exc_info: + WorkflowLaunchForm() + + errors = exc_info.value.errors() + assert any(error["loc"] == ("pipeline",) for error in errors) + + def test_pipeline_cannot_be_empty(self): + """Test that pipeline cannot be empty string.""" + with pytest.raises(ValidationError, match="pipeline is required"): + WorkflowLaunchForm(pipeline="") + + def test_pipeline_whitespace_stripped(self): + """Test that pipeline whitespace is stripped.""" + form = WorkflowLaunchForm(pipeline=" https://github.com/test/repo ") + assert form.pipeline == "https://github.com/test/repo" + + def test_extra_fields_forbidden(self): + """Test that extra fields are not allowed.""" + with pytest.raises(ValidationError): + WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + extraField="not allowed" + ) + + +class TestWorkflowLaunchPayload: + """Tests for WorkflowLaunchPayload schema.""" + + def test_valid_payload_with_launch_only(self): + """Test payload with only launch data.""" + payload = WorkflowLaunchPayload( + launch={"pipeline": "https://github.com/test/repo"} + ) + + assert payload.launch.pipeline == "https://github.com/test/repo" + assert payload.datasetId is None + assert payload.formData is None + + def test_valid_payload_with_dataset_id(self): + """Test payload with dataset ID.""" + payload = WorkflowLaunchPayload( + launch={"pipeline": "https://github.com/test/repo"}, + datasetId="dataset_123", + ) + + assert payload.datasetId == "dataset_123" + + def test_valid_payload_with_form_data(self): + """Test payload with form data.""" + form_data = { + "sample": "test", + "input": "/path/to/file", + "param": 42, + } + payload = WorkflowLaunchPayload( + launch={"pipeline": "https://github.com/test/repo"}, + formData=form_data, + ) + + assert payload.formData == form_data + + def test_extra_fields_forbidden(self): + """Test that extra fields are not allowed.""" + with pytest.raises(ValidationError): + WorkflowLaunchPayload( + launch={"pipeline": "https://github.com/test/repo"}, + unknownField="value" + ) + + +class TestWorkflowLaunchResponse: + """Tests for WorkflowLaunchResponse schema.""" + + def test_valid_response(self): + """Test creating a valid launch response.""" + response = WorkflowLaunchResponse( + message="Workflow launched", + runId="run_123", + status="submitted", + submitTime=datetime(2024, 1, 1, 12, 0, 0), + ) + + assert response.message == "Workflow launched" + assert response.runId == "run_123" + assert response.status == "submitted" + assert response.submitTime.year == 2024 + + +class TestCancelWorkflowResponse: + """Tests for CancelWorkflowResponse schema.""" + + def test_valid_cancel_response(self): + """Test creating a valid cancel response.""" + response = CancelWorkflowResponse( + message="Cancelled", + runId="run_123", + status="cancelled", + ) + + assert response.message == "Cancelled" + assert response.runId == "run_123" + assert response.status == "cancelled" + + +class TestRunInfo: + """Tests for RunInfo schema.""" + + def test_valid_run_info(self): + """Test creating valid run info.""" + run_info = RunInfo( + id="run_123", + run="test-run", + workflow="test-workflow", + status="running", + date="2024-01-01", + cancel="false", + ) + + assert run_info.id == "run_123" + assert run_info.status == "running" + + +class TestListRunsResponse: + """Tests for ListRunsResponse schema.""" + + def test_empty_runs_list(self): + """Test response with empty runs list.""" + response = ListRunsResponse( + runs=[], + total=0, + limit=50, + offset=0, + ) + + assert response.runs == [] + assert response.total == 0 + + def test_runs_list_with_data(self): + """Test response with run data.""" + run_info = RunInfo( + id="run_123", + run="test", + workflow="wf", + status="done", + date="2024-01-01", + cancel="false", + ) + response = ListRunsResponse( + runs=[run_info], + total=1, + limit=50, + offset=0, + ) + + assert len(response.runs) == 1 + assert response.total == 1 + + +class TestLaunchLogs: + """Tests for LaunchLogs schema.""" + + def test_valid_logs(self): + """Test creating valid launch logs.""" + logs = LaunchLogs( + truncated=False, + entries=["log line 1", "log line 2"], + rewindToken="token1", + forwardToken="token2", + pending=False, + message="Logs retrieved", + ) + + assert len(logs.entries) == 2 + assert logs.truncated is False + + +class TestLaunchDetails: + """Tests for LaunchDetails schema.""" + + def test_valid_details(self): + """Test creating valid launch details.""" + details = LaunchDetails( + requiresAttention=False, + status="completed", + ownerId=123, + repository="https://github.com/test/repo", + id="launch_123", + submit="2024-01-01T12:00:00", + start="2024-01-01T12:01:00", + complete="2024-01-01T12:10:00", + dateCreated="2024-01-01T12:00:00", + lastUpdated="2024-01-01T12:10:00", + runName="test-run", + sessionId="session_123", + profile="standard", + workDir="/work", + commitId="abc123", + userName="testuser", + scriptId="script_123", + revision="main", + commandLine="nextflow run", + projectName="test-project", + scriptName="main.nf", + launchId="launch_123", + configFiles=["nextflow.config"], + params={"test": "value"}, + ) + + assert details.status == "completed" + assert details.ownerId == 123 diff --git a/tests/test_services_datasets.py b/tests/test_services_datasets.py new file mode 100644 index 0000000..d51e496 --- /dev/null +++ b/tests/test_services_datasets.py @@ -0,0 +1,325 @@ +"""Tests for dataset service.""" +from __future__ import annotations + +import json +from unittest.mock import AsyncMock, patch + +import pytest + +from app.services.datasets import ( + DatasetCreationResult, + DatasetUploadResult, + SeqeraConfigurationError, + SeqeraServiceError, + _get_required_env, + _stringify_field, + convert_form_data_to_csv, + create_seqera_dataset, + upload_dataset_to_seqera, +) + + +class TestStringifyField: + """Tests for _stringify_field helper.""" + + def test_stringify_none(self): + """Test stringifying None returns empty string.""" + assert _stringify_field(None) == "" + + def test_stringify_string(self): + """Test stringifying a string.""" + assert _stringify_field("hello") == "hello" + + def test_stringify_number(self): + """Test stringifying a number.""" + assert _stringify_field(42) == "42" + assert _stringify_field(3.14) == "3.14" + + def test_stringify_list(self): + """Test stringifying a list.""" + assert _stringify_field(["a", "b", "c"]) == "a;b;c" + + def test_stringify_list_with_none(self): + """Test stringifying a list containing None.""" + assert _stringify_field(["a", None, "c"]) == "a;;c" + + def test_stringify_dict(self): + """Test stringifying a dict as JSON.""" + result = _stringify_field({"key": "value", "num": 42}) + parsed = json.loads(result) + assert parsed["key"] == "value" + assert parsed["num"] == 42 + + def test_stringify_boolean(self): + """Test stringifying boolean.""" + assert _stringify_field(True) == "True" + assert _stringify_field(False) == "False" + + +class TestConvertFormDataToCsv: + """Tests for convert_form_data_to_csv function.""" + + def test_convert_simple_data(self): + """Test converting simple form data to CSV.""" + form_data = { + "name": "test", + "value": "123", + "flag": "true", + } + + csv_output = convert_form_data_to_csv(form_data) + + lines = csv_output.strip().split("\n") + assert len(lines) == 2 # header + 1 data row + assert "name" in lines[0] + assert "value" in lines[0] + assert "flag" in lines[0] + assert "test" in lines[1] + assert "123" in lines[1] + + def test_convert_with_numbers(self): + """Test converting data with numeric values.""" + form_data = { + "sample_id": "sample_001", + "count": 42, + "ratio": 3.14, + } + + csv_output = convert_form_data_to_csv(form_data) + + assert "42" in csv_output + assert "3.14" in csv_output + + def test_convert_with_list(self): + """Test converting data with list values.""" + form_data = { + "sample": "test", + "files": ["file1.txt", "file2.txt"], + } + + csv_output = convert_form_data_to_csv(form_data) + + assert "file1.txt;file2.txt" in csv_output + + def test_convert_with_dict(self): + """Test converting data with dict values.""" + form_data = { + "sample": "test", + "metadata": {"type": "experiment", "id": 1}, + } + + csv_output = convert_form_data_to_csv(form_data) + + assert "metadata" in csv_output + assert "type" in csv_output or "experiment" in csv_output + + def test_convert_empty_data_raises_error(self): + """Test that empty form data raises ValueError.""" + with pytest.raises(ValueError, match="formData cannot be empty"): + convert_form_data_to_csv({}) + + def test_convert_with_none_values(self): + """Test converting data with None values.""" + form_data = { + "sample": "test", + "optional_field": None, + } + + csv_output = convert_form_data_to_csv(form_data) + + lines = csv_output.strip().split("\n") + assert len(lines) == 2 + + +class TestCreateSeqeraDataset: + """Tests for create_seqera_dataset function.""" + + @patch("app.services.datasets.httpx.AsyncClient") + async def test_create_dataset_success(self, mock_client_class): + """Test successful dataset creation.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = { + "id": "dataset_123", + "name": "test-dataset", + } + mock_response.status_code = 200 + mock_response.text = "" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + result = await create_seqera_dataset( + name="test-dataset", + description="Test description" + ) + + assert isinstance(result, DatasetCreationResult) + assert result.dataset_id == "dataset_123" + assert result.raw_response["name"] == "test-dataset" + + @patch("app.services.datasets.httpx.AsyncClient") + async def test_create_dataset_default_name(self, mock_client_class): + """Test dataset creation with auto-generated name.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = { + "id": "dataset_456", + "name": "dataset-1234567890", + } + mock_response.status_code = 200 + mock_response.text = "" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + result = await create_seqera_dataset() + + assert result.dataset_id == "dataset_456" + # Verify a name was generated + mock_client.post.assert_called_once() + + @patch("app.services.datasets.httpx.AsyncClient") + async def test_create_dataset_api_error(self, mock_client_class): + """Test dataset creation with API error.""" + mock_response = AsyncMock() + mock_response.is_error = True + mock_response.status_code = 400 + mock_response.text = "Bad request" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + with pytest.raises(SeqeraServiceError, match="400"): + await create_seqera_dataset(name="test") + + @patch("app.services.datasets.httpx.AsyncClient") + async def test_create_dataset_missing_id_in_response(self, mock_client_class): + """Test handling when response is missing dataset ID.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = { + "name": "test-dataset", + # Missing "id" field + } + mock_response.status_code = 200 + mock_response.text = "{}" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + with pytest.raises(SeqeraServiceError, match="Failed to extract dataset ID"): + await create_seqera_dataset(name="test") + + +class TestUploadDatasetToSeqera: + """Tests for upload_dataset_to_seqera function.""" + + @patch("app.services.datasets.httpx.AsyncClient") + async def test_upload_success(self, mock_client_class): + """Test successful dataset upload.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.status_code = 200 + mock_response.text = "" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form_data = { + "sample": "test_sample", + "input": "/path/file.txt", + } + + result = await upload_dataset_to_seqera( + dataset_id="dataset_789", + form_data=form_data + ) + + assert isinstance(result, DatasetUploadResult) + assert result.success is True + assert result.dataset_id == "dataset_789" + + @patch("app.services.datasets.httpx.AsyncClient") + async def test_upload_creates_csv(self, mock_client_class): + """Test that upload creates proper CSV.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.status_code = 200 + mock_response.text = "" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form_data = { + "col1": "value1", + "col2": "value2", + } + + await upload_dataset_to_seqera("dataset_123", form_data) + + # Verify POST was called with files + call_args = mock_client.post.call_args + assert "files" in call_args[1] + + @patch("app.services.datasets.httpx.AsyncClient") + async def test_upload_api_error(self, mock_client_class): + """Test upload with API error.""" + mock_response = AsyncMock() + mock_response.is_error = True + mock_response.status_code = 500 + mock_response.text = "Server error" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form_data = {"sample": "test"} + + with pytest.raises(SeqeraServiceError, match="500"): + await upload_dataset_to_seqera("dataset_123", form_data) + + @patch("app.services.datasets.httpx.AsyncClient") + async def test_upload_with_complex_data(self, mock_client_class): + """Test upload with complex form data.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.status_code = 200 + mock_response.text = "" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form_data = { + "sample": "test", + "files": ["file1.txt", "file2.txt"], + "count": 42, + "metadata": {"type": "test"}, + } + + result = await upload_dataset_to_seqera("dataset_123", form_data) + + assert result.success is True diff --git a/tests/test_services_seqera.py b/tests/test_services_seqera.py new file mode 100644 index 0000000..9d37219 --- /dev/null +++ b/tests/test_services_seqera.py @@ -0,0 +1,235 @@ +"""Tests for Seqera service.""" +from __future__ import annotations + +import os +from unittest.mock import AsyncMock, patch + +import pytest + +from app.services.seqera import ( + SeqeraConfigurationError, + SeqeraLaunchResult, + SeqeraServiceError, + _get_required_env, + launch_seqera_workflow, +) +from app.schemas.workflows import WorkflowLaunchForm + + +class TestGetRequiredEnv: + """Tests for _get_required_env helper.""" + + def test_get_existing_env_variable(self): + """Test getting an existing environment variable.""" + result = _get_required_env("SEQERA_API_URL") + assert result == "https://api.seqera.test" + + def test_get_missing_env_variable(self): + """Test that missing env variable raises error.""" + with pytest.raises(SeqeraConfigurationError, match="MISSING_VAR"): + _get_required_env("MISSING_VAR") + + +class TestLaunchSeqeraWorkflow: + """Tests for launch_seqera_workflow function.""" + + @patch("app.services.seqera.httpx.AsyncClient") + async def test_launch_success_minimal(self, mock_client_class): + """Test successful workflow launch with minimal parameters.""" + # Setup mock + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = { + "workflowId": "wf_test_123", + } + mock_response.reason_phrase = "OK" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + # Create form + form = WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + ) + + # Execute + result = await launch_seqera_workflow(form) + + # Verify + assert isinstance(result, SeqeraLaunchResult) + assert result.workflow_id == "wf_test_123" + assert result.status == "submitted" + + # Verify API call + mock_client.post.assert_called_once() + call_args = mock_client.post.call_args + assert "https://api.seqera.test/workflow/launch" in call_args[0][0] + + @patch("app.services.seqera.httpx.AsyncClient") + async def test_launch_success_with_all_params(self, mock_client_class): + """Test successful launch with all parameters.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = { + "workflowId": "wf_full_456", + } + mock_response.reason_phrase = "OK" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form = WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + revision="main", + runName="my-custom-run", + configProfiles=["docker", "test"], + paramsText="custom_param: value", + ) + + result = await launch_seqera_workflow(form, dataset_id="dataset_789") + + assert result.workflow_id == "wf_full_456" + + # Verify the payload includes dataset + call_args = mock_client.post.call_args + payload = call_args[1]["json"] + assert "datasetIds" in payload["launch"] + assert "dataset_789" in payload["launch"]["datasetIds"] + + @patch("app.services.seqera.httpx.AsyncClient") + async def test_launch_includes_default_params(self, mock_client_class): + """Test that default parameters are included.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = {"workflowId": "wf_123"} + mock_response.reason_phrase = "OK" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + await launch_seqera_workflow(form) + + call_args = mock_client.post.call_args + payload = call_args[1]["json"] + params_text = payload["launch"]["paramsText"] + + # Check default params are included + assert "use_dgxa100: false" in params_text + assert "project: \"za08\"" in params_text + assert "outdir:" in params_text + + @patch("app.services.seqera.httpx.AsyncClient") + async def test_launch_with_dataset_adds_input_url(self, mock_client_class): + """Test that dataset ID adds input URL to params.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = {"workflowId": "wf_123"} + mock_response.reason_phrase = "OK" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + await launch_seqera_workflow(form, dataset_id="ds_abc") + + call_args = mock_client.post.call_args + payload = call_args[1]["json"] + params_text = payload["launch"]["paramsText"] + + assert "input:" in params_text + assert "ds_abc" in params_text + assert "samplesheet.csv" in params_text + + @patch("app.services.seqera.httpx.AsyncClient") + async def test_launch_api_error_response(self, mock_client_class): + """Test handling of API error response.""" + mock_response = AsyncMock() + mock_response.is_error = True + mock_response.status_code = 400 + mock_response.text = "Invalid request" + mock_response.reason_phrase = "Bad Request" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + with pytest.raises(SeqeraServiceError, match="400"): + await launch_seqera_workflow(form) + + @patch("app.services.seqera.httpx.AsyncClient") + async def test_launch_missing_workflow_id_in_response(self, mock_client_class): + """Test handling when response is missing workflowId.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = {} # No workflowId + mock_response.reason_phrase = "OK" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + with pytest.raises(SeqeraServiceError, match="workflowId"): + await launch_seqera_workflow(form) + + def test_launch_missing_env_vars(self): + """Test that missing environment variables raise error.""" + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + with patch.dict(os.environ, {}, clear=True): + with pytest.raises(SeqeraConfigurationError): + # This will fail synchronously when trying to get env vars + import asyncio + asyncio.run(launch_seqera_workflow(form)) + + @patch("app.services.seqera.httpx.AsyncClient") + async def test_launch_with_custom_params_text(self, mock_client_class): + """Test that custom paramsText is appended to defaults.""" + mock_response = AsyncMock() + mock_response.is_error = False + mock_response.json.return_value = {"workflowId": "wf_123"} + mock_response.reason_phrase = "OK" + + mock_client = AsyncMock() + mock_client.post = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + form = WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + paramsText="my_custom_param: 42\nanother_param: test" + ) + + await launch_seqera_workflow(form) + + call_args = mock_client.post.call_args + payload = call_args[1]["json"] + params_text = payload["launch"]["paramsText"] + + # Should contain both default and custom params + assert "use_dgxa100: false" in params_text # default + assert "my_custom_param: 42" in params_text # custom + assert "another_param: test" in params_text # custom From f9bdf91d32d6ae213654da60b7e7155e329f294e Mon Sep 17 00:00:00 2001 From: laclac102 Date: Thu, 18 Dec 2025 13:58:26 +1100 Subject: [PATCH 03/15] feat: update CI --- .github/workflows/test-coverage.yml | 4 +-- tests/test_additional_coverage.py | 4 +-- tests/test_services_datasets.py | 42 ++++++++++++++++++----------- tests/test_services_seqera.py | 34 +++++++++++------------ 4 files changed, 47 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 9a71984..c6e4c8e 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -62,10 +62,8 @@ jobs: badge: true fail_below_min: true format: markdown - thresholds: '90 95' - hide_branch_rate: false hide_complexity: true indicators: true thresholds: "90 90" - output: console + output: both diff --git a/tests/test_additional_coverage.py b/tests/test_additional_coverage.py index 9da9885..d796527 100644 --- a/tests/test_additional_coverage.py +++ b/tests/test_additional_coverage.py @@ -55,7 +55,7 @@ async def test_get_details_returns_placeholder(self): result = await get_details("run_abc123") assert result.id == "run_abc123" - assert result.status == "pending" - assert result.runName == "N/A" + assert result.status == "UNKNOWN" + assert result.runName == "" assert isinstance(result.configFiles, list) assert isinstance(result.params, dict) diff --git a/tests/test_services_datasets.py b/tests/test_services_datasets.py index d51e496..5b44b39 100644 --- a/tests/test_services_datasets.py +++ b/tests/test_services_datasets.py @@ -2,7 +2,7 @@ from __future__ import annotations import json -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -137,11 +137,13 @@ class TestCreateSeqeraDataset: @patch("app.services.datasets.httpx.AsyncClient") async def test_create_dataset_success(self, mock_client_class): """Test successful dataset creation.""" - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.json.return_value = { - "id": "dataset_123", - "name": "test-dataset", + "dataset": { + "id": "dataset_123", + "name": "test-dataset", + } } mock_response.status_code = 200 mock_response.text = "" @@ -159,16 +161,18 @@ async def test_create_dataset_success(self, mock_client_class): assert isinstance(result, DatasetCreationResult) assert result.dataset_id == "dataset_123" - assert result.raw_response["name"] == "test-dataset" + assert result.raw_response["dataset"]["name"] == "test-dataset" @patch("app.services.datasets.httpx.AsyncClient") async def test_create_dataset_default_name(self, mock_client_class): """Test dataset creation with auto-generated name.""" - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.json.return_value = { - "id": "dataset_456", - "name": "dataset-1234567890", + "dataset": { + "id": "dataset_456", + "name": "dataset-1234567890", + } } mock_response.status_code = 200 mock_response.text = "" @@ -205,11 +209,13 @@ async def test_create_dataset_api_error(self, mock_client_class): @patch("app.services.datasets.httpx.AsyncClient") async def test_create_dataset_missing_id_in_response(self, mock_client_class): """Test handling when response is missing dataset ID.""" - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.json.return_value = { - "name": "test-dataset", - # Missing "id" field + "dataset": { + "name": "test-dataset", + # Missing "id" field + } } mock_response.status_code = 200 mock_response.text = "{}" @@ -220,7 +226,7 @@ async def test_create_dataset_missing_id_in_response(self, mock_client_class): mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - with pytest.raises(SeqeraServiceError, match="Failed to extract dataset ID"): + with pytest.raises(SeqeraServiceError, match="response lacked dataset id"): await create_seqera_dataset(name="test") @@ -230,10 +236,16 @@ class TestUploadDatasetToSeqera: @patch("app.services.datasets.httpx.AsyncClient") async def test_upload_success(self, mock_client_class): """Test successful dataset upload.""" - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.status_code = 200 mock_response.text = "" + mock_response.json.return_value = { + "version": { + "datasetId": "dataset_789" + }, + "message": "Upload successful" + } mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) @@ -258,7 +270,7 @@ async def test_upload_success(self, mock_client_class): @patch("app.services.datasets.httpx.AsyncClient") async def test_upload_creates_csv(self, mock_client_class): """Test that upload creates proper CSV.""" - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.status_code = 200 mock_response.text = "" @@ -302,7 +314,7 @@ async def test_upload_api_error(self, mock_client_class): @patch("app.services.datasets.httpx.AsyncClient") async def test_upload_with_complex_data(self, mock_client_class): """Test upload with complex form data.""" - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.status_code = 200 mock_response.text = "" diff --git a/tests/test_services_seqera.py b/tests/test_services_seqera.py index 9d37219..5c92a71 100644 --- a/tests/test_services_seqera.py +++ b/tests/test_services_seqera.py @@ -2,7 +2,7 @@ from __future__ import annotations import os -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -37,7 +37,7 @@ class TestLaunchSeqeraWorkflow: async def test_launch_success_minimal(self, mock_client_class): """Test successful workflow launch with minimal parameters.""" # Setup mock - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.json.return_value = { "workflowId": "wf_test_123", @@ -71,7 +71,7 @@ async def test_launch_success_minimal(self, mock_client_class): @patch("app.services.seqera.httpx.AsyncClient") async def test_launch_success_with_all_params(self, mock_client_class): """Test successful launch with all parameters.""" - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.json.return_value = { "workflowId": "wf_full_456", @@ -105,12 +105,12 @@ async def test_launch_success_with_all_params(self, mock_client_class): @patch("app.services.seqera.httpx.AsyncClient") async def test_launch_includes_default_params(self, mock_client_class): """Test that default parameters are included.""" - mock_response = AsyncMock() + mock_response = MagicMock() mock_response.is_error = False mock_response.json.return_value = {"workflowId": "wf_123"} mock_response.reason_phrase = "OK" - mock_client = AsyncMock() + mock_client = MagicMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) @@ -131,13 +131,13 @@ async def test_launch_includes_default_params(self, mock_client_class): @patch("app.services.seqera.httpx.AsyncClient") async def test_launch_with_dataset_adds_input_url(self, mock_client_class): - """Test that dataset ID adds input URL to params.""" - mock_response = AsyncMock() + """Test that providing a dataset ID adds it to launch payload.""" + mock_response = MagicMock() mock_response.is_error = False - mock_response.json.return_value = {"workflowId": "wf_123"} + mock_response.json.return_value = {"workflowId": "wf_dataset_999"} mock_response.reason_phrase = "OK" - mock_client = AsyncMock() + mock_client = MagicMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) @@ -177,13 +177,13 @@ async def test_launch_api_error_response(self, mock_client_class): @patch("app.services.seqera.httpx.AsyncClient") async def test_launch_missing_workflow_id_in_response(self, mock_client_class): - """Test handling when response is missing workflowId.""" - mock_response = AsyncMock() + """Test error handling when API response lacks workflowId.""" + mock_response = MagicMock() mock_response.is_error = False - mock_response.json.return_value = {} # No workflowId + mock_response.json.return_value = {"status": "success"} mock_response.reason_phrase = "OK" - mock_client = AsyncMock() + mock_client = MagicMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) @@ -206,13 +206,13 @@ def test_launch_missing_env_vars(self): @patch("app.services.seqera.httpx.AsyncClient") async def test_launch_with_custom_params_text(self, mock_client_class): - """Test that custom paramsText is appended to defaults.""" - mock_response = AsyncMock() + """Test launch with custom paramsText.""" + mock_response = MagicMock() mock_response.is_error = False - mock_response.json.return_value = {"workflowId": "wf_123"} + mock_response.json.return_value = {"workflowId": "wf_params_xyz"} mock_response.reason_phrase = "OK" - mock_client = AsyncMock() + mock_client = MagicMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) From 8abc2518b3a1b4f63e80de016f56d55dce7afd53 Mon Sep 17 00:00:00 2001 From: laclac102 Date: Thu, 18 Dec 2025 14:02:49 +1100 Subject: [PATCH 04/15] fix: improve lint --- .coverage | Bin 69632 -> 0 bytes app/main.py | 5 +- app/routes/workflows.py | 37 ++++++------- app/schemas/workflows.py | 39 ++++++------- app/services/datasets.py | 33 ++++------- app/services/seqera.py | 43 ++++++--------- tests/conftest.py | 7 ++- tests/test_additional_coverage.py | 13 ++--- tests/test_main.py | 27 +++++---- tests/test_routes_workflows.py | 52 ++++++++--------- tests/test_schemas.py | 39 ++++++------- tests/test_services_datasets.py | 89 ++++++++++++++---------------- tests/test_services_seqera.py | 72 ++++++++++++------------ 13 files changed, 214 insertions(+), 242 deletions(-) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index 22c564d2b71b37cc68e9c4177e5ec2379f1cc6c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69632 zcmeI4d3+V+)yJP_?%bI(bMKuYkVsf_AwUQugs_H1HlZxCgncJu0|MDd0)*X}iK15A zTKDy>R%vTni>+^MskPQxYkjL#Yr!gN)ryw3w6)f%HSc-ux#xndz90I}yZFx4T)xjN z_s;xg=69a+JSSPaaBf{&ZE{0XOG9N_a-`@Yq!d$wJ z3_JL4!d)if#zEn(ODu4U<2NS;#eN=dur7_=YF1j+xFURo3WN%T3WN&$|5m_T7&p83 z=pp^RZIxB^wQVhx)wM13o;YvOjHydzB$rH`Hg`snmL&({V z>gsEgb&WN3)s<~^jT@6~n`(LMZLPI6D$wCF40US5VX6HURn*nsUTw7-@gtgB>KZCr zb|$yf?kq}ekp8x|+V(bDfFDv@x3N)eksO@fy)fBQyP>wFwz0alm5yR?T}`1~80@#r z?%le{1GTAhs%)vo|6?CH8(eVuRAz6$52|izsZmE?)l%75y{WdfC|Q{~!|EnH&h|Ds z+m@z=lD4K~eO+VfG#gv%+UlAbleO)&)!W)?Yd-b_iV($Qp6Ty8 zL!Hw*f6Sig&C@$?z=LaSVp&h`sJOjmD;;q&f+_0|v&7?&G>i zfp=wXd+XME>~B?6Zfk2wwJY%ASCrygydw+D?s<9gz}D1_qU5Qls%vY_fc24^CbRex zZXLD$lkXe#lzj9iQBP5QeX?mo>XA`sv}y_ULTT5ItEK`-FVHf(FGR*y=nI*00}?X@kH8*7K9*OD##Pv1y*>y%nX z_ZfMgbn9epfr`=$+FlRC?B1)F^sUrwl)hT&f6R~ELVP7Z#zuKnLv3a2wiY&$|H>qJ zkJB#6&q#U=o=&l8bf4PmlTIhST}5dcz%zBTdrpq@=!sCiq<*3wISIAo^ds;Of#B>9 zzqzt)Q}UmWT`k4~FNhl5C**w6fvZEPC{^!9uv$wU@_`;H^&8Oi)lcLjhpd*IKIHRj zcNTB2tVcI#uB>apM;1MpB0y4YSW}4`S5`G`YfIg~s+qbyRqQ!^yz~L5+pIlqRhZfw zAMf&}*48=ntb{* zT6e0?*toqQbxa&wl+7=}hmM~wgocL7#+r1zQel`r6}?|Qoa%eXL~zGs0X~$i_=x_) zXYz-p*DA;2&mY!|ny620sH~yKU0swjK5@sU+JE{uibm5b+iKhD8fuejlhnbaZmb=Z zt;y<^T3m^-w6GxZU)=`kYE_h~cadJX}((FETsPwj_A4c+-idGEj4V8S@>8AFP zo8%u>p1xlepy6U~=gj*=X?KPks!89V%#E&K6{)~mrTaQ|VFHg*y6+48KYWD>gbIWT zgbIWTgbIWTgbIWTgbIWTgbIWTgbIB66o^PonkxU-+*gGA2Yd@(p#q@-p#q@-p#q@- zp#q@-p#q@-p#q@-p#q@-|2GwgYmq)Kmw1%AGSaJix(HzGuu;QGZ9FT@Jt5o^?qB|I zo?v*?p#q@-p#q@-p#q@-p#q@-p#q@-p#q@-p#q@-tUx@{NAf6uW=DEi=|KQB|8FJ! zD%>aBo83xxp!244kJIFgz(wIJR3KC!R3KC!R3KC!R3KC!R3KC!R3KE~|Gom_tcciI zva}U{!@gvD^QOw-Wo4sFTC1AXPjOXc^_JSknv%+9Y}dAIufsoSgRm9eu>PYw_=c;Lhg1x^qQEU1MEaMTOegINP05zh2*3vZJYG%ZB=<9qOCMMK&{UF-f@w$R64!y8DBe0i-_v8Ipl`Qx<)+35i%mSoXz%U ze21Fu0Ovypx#XNW~jHQ zdehZgM7?S1E&ccZCFq)P-*x}sEO3syN%wX4DffBzNA7prue*1;H@KI(f!ppjxK-{{ zcey*)9peslb6neb+v#-PbRKh_alYpqcAA~LotvGjoQs^@POY=b8RtxQhB^68u9I-I z#Jh>#Tl4K7#6A<-8<&Y?iPsWOCXOW@Nj#AFTH=nx^@)QCKd~cGpQucnpO}&uk{F%H z!@o_K9se-?=lIj{6Y&@0KaSrO|3>^s{HFM>_=WK+_p!6=sP#ANch)P`FRY(fJFM?oU$t(t zuCh9;I%|zJ#TsD^u+Fgn-h-2H9G-%Q;hS(5Tn`>>hfS~s%3(T`K@lV&8^HX)JY~LS z9y5PtK5X7^e%ZX$yxP3T++{YJRpv5tjycI3VGc5TnvSU%?-*|x$Bn0rM~&|oUo*a7 zTx(ov>@!-74aO>CzA?=hV+=9+8eI(|`d;++(O*ZOi#{HGFnTz8SMnTlK5;i}YQ3qh6&i)92`u^bz_X zy{GQznm$S&toPQt=z8Sc$jQjdk!K^1MUF)7jocZzK5|*)f=F9rO(ZW;9+@5~ixfq= zMNI8|?N#lU+IH=`+E=yPw5zn>fA1p_9@%LHcrQyu)0+Q2d2?LAiLF zdHxDHB*P2DpPA2pK%8QpbH4Zk^Xxg|_sp|qi{CNNoG<>E;aTD&^Yn+sTg>N87jH68 zJ5QWoo;ppu!8~QE_zm;qDdKhJNi)SO8J;X&W1jGsc$skN+* zFEWpQQasN*YP2}UJaUvc$~I9LMez)C zVS)G=^WZ}9AIt?q#giExES_KhP#S?XLcR&EoLW6+@E1re3Lohi2Imh32~U&&KJ*RI3~WqY}(@M%u!Q( zjaiS1uQF@8_==4q`xcJuYvLY-($b7h@nzoP9b4SX{CZ#UL*^Ir#beA*mWpSYA1f0t zFh4v=yvqE*Oz~Uh!}G5{ zN^h@^KQ8W0Z?B6(;!g3!OzUpd&b00lcZ)kRtuKnZ#1}Fx+)Mo1=~idExGU4T5kK?x zOzQ@5qqr^8x=!36Zq2j~ip#{!nbu|EI&sSxt%KsGOzX3_<&E6JZ~LscA+F9@;&r*C zQ(Vuh#Kqzgah+NvHBH|yE)dsdw(F=D*JQQ}M2Gl%rsa!3d@j@SgfFhnv@Q^yxQfNw zw_jYzymz0tf_cwgaXItuJ>nqquHE7?=AFC5XPMh~ic6Vyw2Moax9<=aGjH224luWE z6BjYJwuuXwTUtd2^VSv-FgI@%K66vE@R%E$#09B)LQ1`%aX({y!@dk&zn5`K{T{~a zx9m=RgeLVmaq%u*ys2&{5j?DdQ@XH_sBnYoT}#{9L6(`T$<96x?FN%(WB-ujv76e zv1HWwjHM-W7>Ae6W-J*#i?O(5CgYIe&oB-hGJ|o*(CLhYL(a?KsA-IY3a2s-95jXT z_`u1G`O_yc_Rl|;F|Yqb#@xIKjM=&48SU(Gj9`~B8ZeeIYK&pjqoXbK6`b4EwNboK z(?_a>_@CZ6BE34&S(;uJ=^UQkP@CFW!Z>N_FvbZdiWy5M6fq7j9m-hr+z`fL!wVUo zD;mr=bXWo7kfDPZ`wtn&SkQj}cxnECBxC>lJjP`Iz8QSE zPX?2{8DCEJV(fdOCu5(!Ju;ZgW$fLjJ7dq@=P;h=nZuYnDVs4Tw;N+tPFKcmSy_x- zyLDmA>gqDOSq@{uO)$n1aYj29W3+6W5iE<*0AP$7CZirT(pOm1qP$Sk_4LKoIwN3M z>i+1|Oa#1DI&J%??5fQcm-)vuLci8QAy}jOEV$ZTC+QaPucCH<_h4p9a z4eM9dlhzNdf49DZdGP0~1J*99-dbmsThpvjR-x5bzsHzv+-aON=bFvtBQOBgTixIT z%Y;+#8XScu;33SA55diFC3HYL)WZ^(1ruR7`IPy#dAs>}^B(hJ^Mtv_e8K#- z_Di!y`>DCY9A!>1dz)QMUHiUvpE=lg*Lc}DVmxa+X54FBZ>%vcGcGXNj7>(lQDzhw zNh8~U=m*hL(bu9!qfbO1iheVCW%N+==4eN>J-R+xA6*ii6&2Bm(Rg%tbU-v$e?fm+ zKcPRRKcatIzg_>FexbfYuhUoS^YqDjsh+RrM1HHAk-tS=j650nLFB&3b&)SdE{W`p z6hs;#qaziOg^}~L4{LcA>UYYtSmSrS9AA2^?Yh zw)=>CyL++wd3TRn0Iwz=3L;kIcuD9 zrwqqcikxgG=|JLx#3>v}IhuGP@lfKMi9?B-6IUiW674vSvOX~@u_Q4uF+4FKAriTX zc>L}7Q#g+DLi~|UmV{PuZ}N^&x%io7svDBk$6_@FR?dp)Z|#~ z-(o+FeJ}R)*yh-Uu^l*mvcQzw$+e<}G*B6@6;m9R!nji*)lE3U1!Vq zFyPX zFCCCyO?R3AdyZd{_heSxaX@}0v+7Q9hx~G;b%(f9eks$s3D+IUv~CeMN$#qGxLw>L zxswUvHgUV;jw^^;#ch&1t{`p}w@U7~g1AZCEV<(f*crV^a>o_0GkT-suBvL?ExD@- z*crV+a#t0^wc6IV;_xPrJ^Tr0V&3gV#noaC-5h%3ZFc|+!mE*Dow?yIUT zFPGP4)?F$tme*!lmxxOx_f`RWwFf2lRzVyPmq_ldg1AT=klb4Z?A2Z*ugZK1_HyNw znHKhP`ofxE1*cfaH=D;(G>xyuSWJ0*8nq3MX^E-N%PN$#@3<|C53tgvOX zyFqf76{>6H1?gKJs;cFF=E^F$ zk9mEi+?(M>xrce(dP)6OsR53vXXi1M7f@M+~cx> zxon(V$2_J?u4R6Fj9kM!W~^MzJiJt{VlEjjS27ou$Q8^ZkICiC#m~rP87`JfnV%_^ zOPGs_{RhaI%*p=pGt7OHat3qWt8zMX-#mF9^Q(R3H0C}9aw>DrlX434IX&fM z=A3inB<7Pj@?7TZK5`;+R<@kL>}JXF%n4VHV~!J>s7>vwLNxfi8VNu$y3~^ECK3R}yU5Lgu zDAVc?7s`Q|ma@44nU=D-{B-Owgx@I#r@!CWf069RjFy*V-g8j)W!}9<_F>+&TlQvd z-z9r8?`W4jn78kcxy)_bW%snd8m;KBy?8}yn>>fP`H;+JZfcg@n9)yw;UmfPU%`(BfsZPe3H*S(KX7pE^8U58_uBny4TwNngX7pNv8ND{jj9#l_ zk7UQ`G4B61N?%kAVxJF#7^~|ig>ZsNuv+8Pbm6n@nT_vv8x@TIK;JR}% zt%{ilW zjh4u?E))l}c&3Fnw-(Eskhf2>GplegO0&|pn1#bz8gOHu=+OAuS*`muzIN8GZ5m%Y zYv(RaPhUrCN4usmZ{MLw=54K-V8$D|lNs;f51AWV<=>f`8|4Sgct5|-jQ8_<%yo_O zZ_Hcjj595#m@(%6ff;97 ze$Tvmjr<++s@3u&GsgelGGqLIi+TA9`6lzSsQeZ4@R9O)W{kJT zn1_|fqs+y_W>in(8Z`DBI% z%AYY~{QM8*SCaAx=Dfl3r_4R_{2_DBN%=5yc8>f3^T}-a5ObGo`F-ZBQ}RLPE?M$>%%{4@Bg}~|^1I9!SHFYx z|8lB!xtjk!g|+`Ly3e|gyAQedW9Iq=_j;WB?{Ifv9=pL^<1TV%xsxzc9fCCi=eTiK zbKZ4+@4V(b?>yx^ika$R=a6%&^Lgh|%vHBL^-h(u!kLfR>Ud|kQ{ePRc?U=V#B~~QnC#EOHW9B+2(L2#K0ayd@cKogQ%kk&p zPsAU_-1TemJL5NE1;9m^ySBtP#n;7`#OK7P#K**oup%Hg?!@)jd$Cio-^5;sJso>2 z_8?XQd^vV|?Aq96F+bKGYmC*zR>jI=Gq4(^JNe?4R3@+TXSB zwZCZJXkURNAv^6xyV_o1pKnjHOKBv;>co+d-&n`2C#>&VhpjJK*IS>p_E}r48f&>V z$2u3Q0tQ=st!&GJPIw#M#L9qY;4%0f9EQ7bM}%_(uXX z^RMPf^A+qu-6* z8~q|y3S5DsAv>du(dy`m=)CB(=-6mcv>(3swsp z(D&$D^$j>GQm)U?$LYgxTqIXd=vw5jk+*PUu)~PoyQXDY7oI zI5ImjDKauLIMOH56)Ock(Eh0XM*Efal=dU-JKEQ^JGC3|7Q`3$Q=>RiDWOjR_)`db zo&-7aa70Uj27U=G)O6DyrV#G~(=Se~Hj#=_ z%gj#jho&|(Rg7b+yuT&xM3ss7GdoM z@PV+V7JQSix(0lMu&NsTC}Cw4_?SQ_{nl54A0ez*557jYt^#~XxON@*f^f}R@b(j~ zUIX4f!d0um+e^4|6?l6HSF8kYH{qcb;O!z@dKkQ&giDr!*G{;233xjQ7cB;FJK@4b z;B6x;UkF|s;evATS_$Vb0I!Ac@O<#L63#?yCOrQE@R|tcoDW_j;p{o!H4x644PHIr z%vs=VA$(vac$*2QKMYlJO#WO!by|Ct0sJS z5_naFWr&sO4e;C66Ha&xyb8kc6Tn+XIBq<6YYEH7fwzY6u`=*h6P6;bA{_lBcq<7< zjRtQ8;mA?oEhii?61-)Ec0PDZ2}?(Sw}kM?Qt%cN4t){4MTEse!COdJR198udV?bH z77!NT2J;CE3&5L4IJgkJx#X?*$$?v60&yJaS^A zdoFn7#6~tQB_}p=vcV%KHnMZTBPTY{pFMJ7qiYs;dBDJzy%v=cmK#5l(`{#YM#{z}BZI5hN$NGf3Y){!R)&Zy@2isFdtQuIKV0+4nRRc$WZBLo8YT%Hs?I}A} z4J=2nJ!QzMft3ojr!1M)9JD><$*O^s3AU$P85^oF!Sa+Z<2*UmCD@)aX0;Wjf|i%E z=2TUJnY6xh7kFgSdizfB$fR`)Dju1%jzPsElh(Iw2ailz zZ`%eQnY51a#Uqo}TUx;*lh(JkfJY{+H*W=xOj^fK`07;4C)vHAeWqIC>3r_3`6@{8vY>Ci~SV9tUz?eab{2^VKmdkoW2s707z^ zb0+~guRifyAmi1?jsx;tz3e!U?doM^K(4EgEdw%LebiVW&(%jw1hQOx^e7<5)iD5& z;p!!$fc#c3EdjDyeRwI5+v+95fy`DfE&=jdeMm8o)#^is06DEbWGIl)>V-pqd{!?U z1!S}Oph6&*)dvm&GFkojKp>CR^QQw@tlmE#$YJ%o{y+w+=jH+VtDc<;WUsoN4dkvm z*g)p0U0|tU}P9zM1$~KWuC*_$)$^M`WQ_;(y+)_~z zlvN_VoCM{QNPSO$GD)O9eL*>-q9iDPM8YtbGDoTP>R9M9Rqp zWr;{xIiUOysaqD18ERd-0ePW@fsU+D!|a5dP{ZVej8MaPMn0%vI3pX>Ff$<+)G#q2 z6V&jEk_T#dMacp+yrRkhRe2eZ|Dh$DDf_duPD6{T3V=>S;=j`BRKNfKyfICspeeNOm7WZoR0M1Qs#oT|jTkg(q$Gau&0Jn$hxDm|!f9L$#`K9xO^8@F8 z=PSx{sxzqga+7>W0Bp89pn`JepQivMM)n*Y&Q^`Do> zPFSg$e^v4SG}indiQgN)BYtiC63q3R;#KjbshR3w@qY2_xQSW*@37YYnb@PT2V(cc zZi`)wGu7>}EwOd6g|X?eve?jAUMxFi*&o_}w%@dm+t1oRu^+VW!zzbc?5phq_8zSF z-(at{%k3HVc)J9%`yRGqN33_T;{VsyFEPLWfptGtI^1quV_jESljSAJP%L8Bk%xx4er4De@vQS2Ua&!!ZJ7?rodODs8zI!Ho}?>5VR6v z#uBuo5tnWytXK*`Ga)7@K@%ZnC_y734tWI)ggE3C)DvQo5^N#FA+KOFAr5&3b%e`T zK(L7rlb2v4;nHOgY)Io02x9;S0GqKnBO0Qg@glfX?c2sd59t5)qd!K+{79kGm1TzVH_JZIugt-S@))ZNA|*0b)+q=mld-yFK|raDb*c*l6XP)gTy23$X_rj8KRruMm`` z7GeP*1j7k&1TQEd9C8eTVT6T4te_ZCld5>o4vG|OsoFs+7@A&j2d)^BUV)W^c2JmZ zVBw%03{E$&ZqN=2(hV#dw1YwE238H)!N7C_iw5mrK)Qi7gLaUgZeYov9jMet9ROAg z+JVY`R09hJ?LZ|!s-Z@U0+j<%<8C`pX^?8Dp`$=$LaKqqf_9*iA=SWIK|4s9I5x0U z&<@DOQg>yq>-GH{~lXU~yrcc%lOBaJr)(tp4;FER3V%&|a8x}1ApR5}e z%mbgS8*n1PC+miJi@+!AhPm^>C+mhebHOL;1{~+{$+`gtdVI2OI5G=-vTm5Z6MV96 zICnbuWZf|J5%9^n0mu1$vTncu9iOZla74!^>xPMwz$fd5M<#+#)(tqkjoUZ@yWVjWftQ*kVeX?#SECQdb8;%u%Pu30juYyn34LB^}lXXKsTuRmr z{ayl}tQ-3E1D~uL3h>a$y5S{UO4bcYM6zxefS*9t4gCjzPu2}M-r FastAPI: if not allowed_origins_env: raise RuntimeError("ALLOWED_ORIGINS environment variable is required but not set") - allowed_origins = [origin.strip() for origin in allowed_origins_env.split(",") if origin.strip()] + allowed_origins = [ + origin.strip() for origin in allowed_origins_env.split(",") if origin.strip() + ] app.add_middleware( CORSMiddleware, diff --git a/app/routes/workflows.py b/app/routes/workflows.py index 0747832..95f0ebe 100644 --- a/app/routes/workflows.py +++ b/app/routes/workflows.py @@ -1,9 +1,9 @@ """Workflow-related HTTP routes.""" + from __future__ import annotations import asyncio from datetime import datetime, timezone -from typing import Optional from fastapi import APIRouter, HTTPException, Query, status @@ -36,26 +36,23 @@ async def launch_workflow(payload: WorkflowLaunchPayload) -> WorkflowLaunchRespo """Launch a workflow on the Seqera Platform.""" try: dataset_id = payload.datasetId - + # If formData is provided, create and upload dataset if payload.formData: dataset_result = await create_seqera_dataset( name=payload.launch.runName or "workflow-dataset" ) dataset_id = dataset_result.dataset_id - - await upload_dataset_to_seqera( - dataset_id=dataset_id, - form_data=payload.formData - ) - - result: SeqeraLaunchResult = await launch_seqera_workflow( - payload.launch, dataset_id - ) + + await upload_dataset_to_seqera(dataset_id=dataset_id, form_data=payload.formData) + + result: SeqeraLaunchResult = await launch_seqera_workflow(payload.launch, dataset_id) except SeqeraConfigurationError as exc: - raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) + ) from exc except SeqeraServiceError as exc: - raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc return WorkflowLaunchResponse( message="Workflow launched successfully", @@ -77,8 +74,8 @@ async def cancel_workflow(run_id: str) -> CancelWorkflowResponse: @router.get("/runs", response_model=ListRunsResponse) async def list_runs( - status_filter: Optional[str] = Query(None, alias="status"), - workspace: Optional[str] = Query(None), + status_filter: str | None = Query(None, alias="status"), + workspace: str | None = Query(None), limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), ) -> ListRunsResponse: @@ -143,9 +140,9 @@ async def upload_dataset(payload: DatasetUploadRequest) -> DatasetUploadResponse except SeqeraConfigurationError as exc: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) - ) + ) from exc except SeqeraServiceError as exc: - raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc # Allow Seqera time to finish dataset initialization before uploading await asyncio.sleep(2) @@ -153,13 +150,13 @@ async def upload_dataset(payload: DatasetUploadRequest) -> DatasetUploadResponse try: upload_result = await upload_dataset_to_seqera(dataset.dataset_id, payload.formData) except ValueError as exc: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc except SeqeraConfigurationError as exc: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc) - ) + ) from exc except SeqeraServiceError as exc: - raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) + raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc return DatasetUploadResponse( message="Dataset created and uploaded successfully", diff --git a/app/schemas/workflows.py b/app/schemas/workflows.py index 8275fd0..79b09c8 100644 --- a/app/schemas/workflows.py +++ b/app/schemas/workflows.py @@ -1,8 +1,9 @@ """Pydantic models shared across workflow endpoints.""" + from __future__ import annotations from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -11,18 +12,14 @@ class WorkflowLaunchForm(BaseModel): model_config = ConfigDict(extra="forbid") pipeline: str = Field(..., description="Workflow pipeline repository or URL") - revision: Optional[str] = Field( + revision: str | None = Field( default=None, description="Revision or branch of the pipeline to run" ) - configProfiles: List[str] = Field( + configProfiles: list[str] = Field( default_factory=list, description="Profiles that customize the workflow" ) - runName: Optional[str] = Field( - default=None, description="Human-readable workflow run name" - ) - paramsText: Optional[str] = Field( - default=None, description="YAML-style parameter overrides" - ) + runName: str | None = Field(default=None, description="Human-readable workflow run name") + paramsText: str | None = Field(default=None, description="YAML-style parameter overrides") @field_validator("pipeline") @classmethod @@ -36,11 +33,11 @@ class WorkflowLaunchPayload(BaseModel): model_config = ConfigDict(extra="forbid") launch: WorkflowLaunchForm - datasetId: Optional[str] = Field( + datasetId: str | None = Field( default=None, description="Optional Seqera dataset ID to attach to the workflow", ) - formData: Optional[Dict[str, Any]] = Field( + formData: dict[str, Any] | None = Field( default=None, description="Optional form data to convert to CSV and upload as a dataset", ) @@ -69,7 +66,7 @@ class RunInfo(BaseModel): class ListRunsResponse(BaseModel): - runs: List[RunInfo] + runs: list[RunInfo] total: int limit: int offset: int @@ -77,12 +74,12 @@ class ListRunsResponse(BaseModel): class LaunchLogs(BaseModel): truncated: bool - entries: List[str] + entries: list[str] rewindToken: str forwardToken: str pending: bool message: str - downloads: List[Dict[str, str]] = Field(default_factory=list) + downloads: list[dict[str, str]] = Field(default_factory=list) class LaunchDetails(BaseModel): @@ -108,20 +105,20 @@ class LaunchDetails(BaseModel): projectName: str scriptName: str launchId: str - configFiles: List[str] - params: Dict[str, str] + configFiles: list[str] + params: dict[str, str] class DatasetUploadRequest(BaseModel): model_config = ConfigDict(extra="forbid") - formData: Dict[str, Any] - datasetName: Optional[str] = Field(default=None) - datasetDescription: Optional[str] = Field(default=None) + formData: dict[str, Any] + datasetName: str | None = Field(default=None) + datasetDescription: str | None = Field(default=None) @field_validator("formData") @classmethod - def validate_form_data(cls, value: Dict[str, Any]) -> Dict[str, Any]: + def validate_form_data(cls, value: dict[str, Any]) -> dict[str, Any]: if not value: raise ValueError("formData cannot be empty") return value @@ -131,4 +128,4 @@ class DatasetUploadResponse(BaseModel): message: str datasetId: str success: bool - details: Optional[Dict[str, Any]] = None + details: dict[str, Any] | None = None diff --git a/app/services/datasets.py b/app/services/datasets.py index 5b8fc11..69589b7 100644 --- a/app/services/datasets.py +++ b/app/services/datasets.py @@ -1,4 +1,5 @@ """Dataset helpers for interacting with the Seqera Platform.""" + from __future__ import annotations import csv @@ -8,7 +9,7 @@ import os import time from dataclasses import dataclass -from typing import Any, Dict, Optional +from typing import Any import httpx @@ -20,9 +21,7 @@ def _get_required_env(key: str) -> str: value = os.getenv(key) if not value: - raise SeqeraConfigurationError( - f"Missing required environment variable: {key}" - ) + raise SeqeraConfigurationError(f"Missing required environment variable: {key}") return value @@ -36,7 +35,7 @@ def _stringify_field(value: Any) -> str: return str(value) -def convert_form_data_to_csv(form_data: Dict[str, Any]) -> str: +def convert_form_data_to_csv(form_data: dict[str, Any]) -> str: """Convert a record of form data into a single-row CSV string.""" if not form_data: raise ValueError("formData cannot be empty") @@ -54,7 +53,7 @@ def convert_form_data_to_csv(form_data: Dict[str, Any]) -> str: @dataclass class DatasetCreationResult: dataset_id: str - raw_response: Dict[str, Any] + raw_response: dict[str, Any] @dataclass @@ -62,11 +61,11 @@ class DatasetUploadResult: success: bool dataset_id: str message: str - raw_response: Optional[Dict[str, Any]] = None + raw_response: dict[str, Any] | None = None async def create_seqera_dataset( - name: Optional[str] = None, description: Optional[str] = None + name: str | None = None, description: str | None = None ) -> DatasetCreationResult: """Create a dataset on the Seqera Platform.""" seqera_api_url = _get_required_env("SEQERA_API_URL").rstrip("/") @@ -103,23 +102,19 @@ async def create_seqera_dataset( "body": body, }, ) - raise SeqeraServiceError( - f"Seqera dataset creation failed: {response.status_code} {body}" - ) + raise SeqeraServiceError(f"Seqera dataset creation failed: {response.status_code} {body}") data = response.json() dataset_id = data.get("dataset", {}).get("id") if not dataset_id: - raise SeqeraServiceError( - "Seqera dataset creation succeeded but response lacked dataset id" - ) + raise SeqeraServiceError("Seqera dataset creation succeeded but response lacked dataset id") logger.info("Seqera dataset created", extra={"datasetId": dataset_id}) return DatasetCreationResult(dataset_id=dataset_id, raw_response=data) async def upload_dataset_to_seqera( - dataset_id: str, form_data: Dict[str, Any] + dataset_id: str, form_data: dict[str, Any] ) -> DatasetUploadResult: """Upload CSV-encoded form data to an existing Seqera dataset.""" if not dataset_id: @@ -160,14 +155,10 @@ async def upload_dataset_to_seqera( "body": body, }, ) - raise SeqeraServiceError( - f"Seqera dataset upload failed: {response.status_code} {body}" - ) + raise SeqeraServiceError(f"Seqera dataset upload failed: {response.status_code} {body}") data = response.json() - returned_dataset_id = ( - data.get("version", {}).get("datasetId") or dataset_id - ) + returned_dataset_id = data.get("version", {}).get("datasetId") or dataset_id message = data.get("message") or "Upload successful" logger.info( diff --git a/app/services/seqera.py b/app/services/seqera.py index 99975ec..0cc928c 100644 --- a/app/services/seqera.py +++ b/app/services/seqera.py @@ -1,10 +1,11 @@ """Seqera Platform integration helpers.""" + from __future__ import annotations import logging import os from dataclasses import dataclass -from typing import Any, Dict, Optional +from typing import Any import httpx @@ -25,20 +26,18 @@ class SeqeraServiceError(RuntimeError): class SeqeraLaunchResult: workflow_id: str status: str - message: Optional[str] = None + message: str | None = None def _get_required_env(key: str) -> str: value = os.getenv(key) if not value: - raise SeqeraConfigurationError( - f"Missing required environment variable: {key}" - ) + raise SeqeraConfigurationError(f"Missing required environment variable: {key}") return value async def launch_seqera_workflow( - form: WorkflowLaunchForm, dataset_id: Optional[str] = None + form: WorkflowLaunchForm, dataset_id: str | None = None ) -> SeqeraLaunchResult: """Launch a workflow on the Seqera Platform.""" seqera_api_url = _get_required_env("SEQERA_API_URL").rstrip("/") @@ -68,20 +67,20 @@ async def launch_seqera_workflow( "batches: 1", "help: false", ] - + # Start with default parameters params_text = "\n".join(default_params) - + # Add custom paramsText from frontend if provided if form.paramsText and form.paramsText.strip(): params_text = f"{params_text}\n{form.paramsText.rstrip()}" - + # Add dataset input URL if dataset_id is provided if dataset_id: dataset_url = f"{seqera_api_url}/workspaces/{workspace_id}/datasets/{dataset_id}/v/1/n/samplesheet.csv" params_text = f"{params_text}\ninput: {dataset_url}" - launch_payload: Dict[str, Any] = { + launch_payload: dict[str, Any] = { "launch": { "computeEnvId": compute_env_id, "runName": form.runName or "hello-from-ui", @@ -100,18 +99,12 @@ async def launch_seqera_workflow( launch_payload["launch"]["datasetIds"] = [dataset_id] url = f"{seqera_api_url}/workflow/launch?workspaceId={workspace_id}" - + # Log the complete params being sent - logger.info( - "Launch payload paramsText", - extra={"paramsText": params_text} - ) - - logger.info( - "Full launch payload", - extra={"payload": launch_payload} - ) - + logger.info("Launch payload paramsText", extra={"paramsText": params_text}) + + logger.info("Full launch payload", extra={"payload": launch_payload}) + logger.info( "Launching workflow via Seqera API", extra={ @@ -142,18 +135,14 @@ async def launch_seqera_workflow( "body": body, }, ) - raise SeqeraServiceError( - f"Seqera workflow launch failed: {response.status_code} {body}" - ) + raise SeqeraServiceError(f"Seqera workflow launch failed: {response.status_code} {body}") data = response.json() workflow_id = data.get("workflowId") or data.get("data", {}).get("workflowId") status = data.get("status", "submitted") if not workflow_id: - raise SeqeraServiceError( - "Seqera workflow launch succeeded but did not return a workflowId" - ) + raise SeqeraServiceError("Seqera workflow launch succeeded but did not return a workflowId") return SeqeraLaunchResult( workflow_id=workflow_id, diff --git a/tests/conftest.py b/tests/conftest.py index e492955..378c89d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,9 @@ """Shared test fixtures and configuration.""" + from __future__ import annotations import os -from typing import AsyncGenerator, Dict, Generator +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, MagicMock import pytest @@ -43,9 +44,10 @@ async def async_client(app) -> AsyncGenerator[AsyncClient, None]: @pytest.fixture def mock_httpx_response(): """Create a mock httpx Response.""" + def _create_response( status_code: int = 200, - json_data: Dict | None = None, + json_data: dict | None = None, text: str = "", is_error: bool = False, ): @@ -56,6 +58,7 @@ def _create_response( if json_data: response.json.return_value = json_data return response + return _create_response diff --git a/tests/test_additional_coverage.py b/tests/test_additional_coverage.py index d796527..6f266a1 100644 --- a/tests/test_additional_coverage.py +++ b/tests/test_additional_coverage.py @@ -1,10 +1,9 @@ """Additional tests to increase coverage.""" + from __future__ import annotations from unittest.mock import AsyncMock, patch -import pytest - from app.routes.workflows import get_details, upload_dataset from app.schemas.workflows import DatasetUploadRequest from app.services.datasets import DatasetUploadResult @@ -22,7 +21,7 @@ async def test_upload_dataset_success(self, mock_create, mock_upload): mock_create_result.dataset_id = "dataset_123" mock_create_result.raw_response = {"id": "dataset_123"} mock_create.return_value = mock_create_result - + # Mock upload mock_upload_result = DatasetUploadResult( success=True, @@ -30,16 +29,16 @@ async def test_upload_dataset_success(self, mock_create, mock_upload): message="Uploaded", ) mock_upload.return_value = mock_upload_result - + # Create request request = DatasetUploadRequest( formData={"sample": "test"}, datasetName="test-dataset", ) - + # Execute response = await upload_dataset(request) - + # Verify assert response.success is True assert response.datasetId == "dataset_123" @@ -53,7 +52,7 @@ class TestGetDetails: async def test_get_details_returns_placeholder(self): """Test that get_details returns proper placeholder data.""" result = await get_details("run_abc123") - + assert result.id == "run_abc123" assert result.status == "UNKNOWN" assert result.runName == "" diff --git a/tests/test_main.py b/tests/test_main.py index b8ce797..0da91ec 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,5 @@ """Tests for the main FastAPI application.""" + from __future__ import annotations import os @@ -12,9 +13,9 @@ def test_create_app_success(): """Test that create_app creates a valid FastAPI instance.""" from app.main import create_app - + app = create_app() - + assert isinstance(app, FastAPI) assert app.title == "SBP Portal Backend" assert app.version == "1.0.0" @@ -23,7 +24,7 @@ def test_create_app_success(): def test_create_app_missing_allowed_origins(): """Test that create_app raises error when ALLOWED_ORIGINS is missing.""" from app.main import create_app - + with patch.dict(os.environ, {}, clear=True): with pytest.raises(RuntimeError, match="ALLOWED_ORIGINS environment variable is required"): create_app() @@ -32,7 +33,7 @@ def test_create_app_missing_allowed_origins(): def test_health_endpoint(client: TestClient): """Test the /health endpoint returns correct response.""" response = client.get("/health") - + assert response.status_code == 200 data = response.json() assert data["status"] == "ok" @@ -47,14 +48,14 @@ def test_cors_middleware_configured(app: FastAPI): if "CORSMiddleware" in str(middleware): middleware_found = True break - + assert middleware_found, "CORS middleware should be configured" def test_workflow_router_included(app: FastAPI): """Test that workflow router is included with correct prefix.""" route_paths = [route.path for route in app.routes] - + assert "/api/workflows/launch" in route_paths assert "/api/workflows/runs" in route_paths @@ -63,7 +64,7 @@ def test_exception_handler(client: TestClient): """Test that global exception handler works.""" # Try to access a non-existent endpoint response = client.get("/nonexistent") - + # Should return 404 but not crash assert response.status_code == 404 @@ -71,8 +72,10 @@ def test_exception_handler(client: TestClient): def test_cors_allowed_origins_parsing(): """Test that ALLOWED_ORIGINS is correctly parsed from environment.""" from app.main import create_app - - with patch.dict(os.environ, {"ALLOWED_ORIGINS": "http://localhost:3000, http://localhost:4200"}): + + with patch.dict( + os.environ, {"ALLOWED_ORIGINS": "http://localhost:3000, http://localhost:4200"} + ): app = create_app() assert app is not None @@ -80,7 +83,9 @@ def test_cors_allowed_origins_parsing(): def test_cors_allowed_origins_with_empty_values(): """Test that empty values in ALLOWED_ORIGINS are filtered out.""" from app.main import create_app - - with patch.dict(os.environ, {"ALLOWED_ORIGINS": "http://localhost:3000,, , http://localhost:4200"}): + + with patch.dict( + os.environ, {"ALLOWED_ORIGINS": "http://localhost:3000,, , http://localhost:4200"} + ): app = create_app() assert app is not None diff --git a/tests/test_routes_workflows.py b/tests/test_routes_workflows.py index 13818d4..81bd65a 100644 --- a/tests/test_routes_workflows.py +++ b/tests/test_routes_workflows.py @@ -1,9 +1,9 @@ """Tests for workflow routes.""" + from __future__ import annotations from unittest.mock import AsyncMock, patch -import pytest from fastapi.testclient import TestClient from app.services.seqera import ( @@ -24,16 +24,16 @@ async def test_launch_success_without_dataset(self, mock_launch, client: TestCli status="submitted", message="Success", ) - + payload = { "launch": { "pipeline": "https://github.com/test/repo", "runName": "test-run", } } - + response = client.post("/api/workflows/launch", json=payload) - + assert response.status_code == 201 data = response.json() assert data["runId"] == "wf_123" @@ -51,16 +51,16 @@ async def test_launch_success_with_form_data( mock_create_result = AsyncMock() mock_create_result.dataset_id = "dataset_456" mock_create_dataset.return_value = mock_create_result - + # Mock dataset upload mock_upload.return_value = None - + # Mock workflow launch mock_launch.return_value = SeqeraLaunchResult( workflow_id="wf_789", status="submitted", ) - + payload = { "launch": { "pipeline": "https://github.com/test/repo", @@ -69,15 +69,15 @@ async def test_launch_success_with_form_data( "formData": { "sample": "test", "input": "/path/file.txt", - } + }, } - + response = client.post("/api/workflows/launch", json=payload) - + assert response.status_code == 201 data = response.json() assert data["runId"] == "wf_789" - + # Verify dataset creation was called mock_create_dataset.assert_called_once() mock_upload.assert_called_once() @@ -86,15 +86,15 @@ async def test_launch_success_with_form_data( async def test_launch_configuration_error(self, mock_launch, client: TestClient): """Test launch with configuration error.""" mock_launch.side_effect = SeqeraConfigurationError("Missing API token") - + payload = { "launch": { "pipeline": "https://github.com/test/repo", } } - + response = client.post("/api/workflows/launch", json=payload) - + assert response.status_code == 500 assert "Missing API token" in response.json()["detail"] @@ -102,15 +102,15 @@ async def test_launch_configuration_error(self, mock_launch, client: TestClient) async def test_launch_service_error(self, mock_launch, client: TestClient): """Test launch with Seqera service error.""" mock_launch.side_effect = SeqeraServiceError("API returned 502") - + payload = { "launch": { "pipeline": "https://github.com/test/repo", } } - + response = client.post("/api/workflows/launch", json=payload) - + assert response.status_code == 502 assert "API returned 502" in response.json()["detail"] @@ -121,9 +121,9 @@ def test_launch_invalid_payload(self, client: TestClient): "pipeline": "", # Empty pipeline } } - + response = client.post("/api/workflows/launch", json=payload) - + assert response.status_code == 422 # Validation error @@ -133,7 +133,7 @@ class TestCancelWorkflow: def test_cancel_workflow_success(self, client: TestClient): """Test successful workflow cancellation.""" response = client.post("/api/workflows/run_123/cancel") - + assert response.status_code == 200 data = response.json() assert data["runId"] == "run_123" @@ -147,7 +147,7 @@ class TestListRuns: def test_list_runs_default_params(self, client: TestClient): """Test listing runs with default parameters.""" response = client.get("/api/workflows/runs") - + assert response.status_code == 200 data = response.json() assert "runs" in data @@ -164,9 +164,9 @@ def test_list_runs_with_filters(self, client: TestClient): "workspace": "test_ws", "limit": 10, "offset": 5, - } + }, ) - + assert response.status_code == 200 data = response.json() assert data["limit"] == 10 @@ -177,7 +177,7 @@ def test_list_runs_limit_validation(self, client: TestClient): # Test limit too high response = client.get("/api/workflows/runs", params={"limit": 300}) assert response.status_code == 422 - + # Test limit too low response = client.get("/api/workflows/runs", params={"limit": 0}) assert response.status_code == 422 @@ -194,7 +194,7 @@ class TestGetLogs: def test_get_logs_success(self, client: TestClient): """Test successful log retrieval.""" response = client.get("/api/workflows/run_123/logs") - + assert response.status_code == 200 data = response.json() assert "entries" in data @@ -209,7 +209,7 @@ class TestGetDetails: def test_get_details_success(self, client: TestClient): """Test successful details retrieval.""" response = client.get("/api/workflows/run_123/details") - + assert response.status_code == 200 data = response.json() assert data["id"] == "run_123" diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 0d1abbe..0310e64 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1,4 +1,5 @@ """Tests for Pydantic schemas.""" + from __future__ import annotations from datetime import datetime @@ -24,7 +25,7 @@ class TestWorkflowLaunchForm: def test_valid_minimal_form(self): """Test WorkflowLaunchForm with minimal valid data.""" form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - + assert form.pipeline == "https://github.com/test/repo" assert form.revision is None assert form.configProfiles == [] @@ -40,7 +41,7 @@ def test_valid_complete_form(self): runName="my-test-run", paramsText="param1: value1\nparam2: value2", ) - + assert form.pipeline == "https://github.com/test/repo" assert form.revision == "main" assert form.configProfiles == ["docker", "test"] @@ -51,7 +52,7 @@ def test_pipeline_required(self): """Test that pipeline field is required.""" with pytest.raises(ValidationError) as exc_info: WorkflowLaunchForm() - + errors = exc_info.value.errors() assert any(error["loc"] == ("pipeline",) for error in errors) @@ -68,10 +69,7 @@ def test_pipeline_whitespace_stripped(self): def test_extra_fields_forbidden(self): """Test that extra fields are not allowed.""" with pytest.raises(ValidationError): - WorkflowLaunchForm( - pipeline="https://github.com/test/repo", - extraField="not allowed" - ) + WorkflowLaunchForm(pipeline="https://github.com/test/repo", extraField="not allowed") class TestWorkflowLaunchPayload: @@ -79,10 +77,8 @@ class TestWorkflowLaunchPayload: def test_valid_payload_with_launch_only(self): """Test payload with only launch data.""" - payload = WorkflowLaunchPayload( - launch={"pipeline": "https://github.com/test/repo"} - ) - + payload = WorkflowLaunchPayload(launch={"pipeline": "https://github.com/test/repo"}) + assert payload.launch.pipeline == "https://github.com/test/repo" assert payload.datasetId is None assert payload.formData is None @@ -93,7 +89,7 @@ def test_valid_payload_with_dataset_id(self): launch={"pipeline": "https://github.com/test/repo"}, datasetId="dataset_123", ) - + assert payload.datasetId == "dataset_123" def test_valid_payload_with_form_data(self): @@ -107,15 +103,14 @@ def test_valid_payload_with_form_data(self): launch={"pipeline": "https://github.com/test/repo"}, formData=form_data, ) - + assert payload.formData == form_data def test_extra_fields_forbidden(self): """Test that extra fields are not allowed.""" with pytest.raises(ValidationError): WorkflowLaunchPayload( - launch={"pipeline": "https://github.com/test/repo"}, - unknownField="value" + launch={"pipeline": "https://github.com/test/repo"}, unknownField="value" ) @@ -130,7 +125,7 @@ def test_valid_response(self): status="submitted", submitTime=datetime(2024, 1, 1, 12, 0, 0), ) - + assert response.message == "Workflow launched" assert response.runId == "run_123" assert response.status == "submitted" @@ -147,7 +142,7 @@ def test_valid_cancel_response(self): runId="run_123", status="cancelled", ) - + assert response.message == "Cancelled" assert response.runId == "run_123" assert response.status == "cancelled" @@ -166,7 +161,7 @@ def test_valid_run_info(self): date="2024-01-01", cancel="false", ) - + assert run_info.id == "run_123" assert run_info.status == "running" @@ -182,7 +177,7 @@ def test_empty_runs_list(self): limit=50, offset=0, ) - + assert response.runs == [] assert response.total == 0 @@ -202,7 +197,7 @@ def test_runs_list_with_data(self): limit=50, offset=0, ) - + assert len(response.runs) == 1 assert response.total == 1 @@ -220,7 +215,7 @@ def test_valid_logs(self): pending=False, message="Logs retrieved", ) - + assert len(logs.entries) == 2 assert logs.truncated is False @@ -256,6 +251,6 @@ def test_valid_details(self): configFiles=["nextflow.config"], params={"test": "value"}, ) - + assert details.status == "completed" assert details.ownerId == 123 diff --git a/tests/test_services_datasets.py b/tests/test_services_datasets.py index 5b44b39..d16df22 100644 --- a/tests/test_services_datasets.py +++ b/tests/test_services_datasets.py @@ -1,4 +1,5 @@ """Tests for dataset service.""" + from __future__ import annotations import json @@ -9,9 +10,7 @@ from app.services.datasets import ( DatasetCreationResult, DatasetUploadResult, - SeqeraConfigurationError, SeqeraServiceError, - _get_required_env, _stringify_field, convert_form_data_to_csv, create_seqera_dataset, @@ -66,9 +65,9 @@ def test_convert_simple_data(self): "value": "123", "flag": "true", } - + csv_output = convert_form_data_to_csv(form_data) - + lines = csv_output.strip().split("\n") assert len(lines) == 2 # header + 1 data row assert "name" in lines[0] @@ -84,9 +83,9 @@ def test_convert_with_numbers(self): "count": 42, "ratio": 3.14, } - + csv_output = convert_form_data_to_csv(form_data) - + assert "42" in csv_output assert "3.14" in csv_output @@ -96,9 +95,9 @@ def test_convert_with_list(self): "sample": "test", "files": ["file1.txt", "file2.txt"], } - + csv_output = convert_form_data_to_csv(form_data) - + assert "file1.txt;file2.txt" in csv_output def test_convert_with_dict(self): @@ -107,9 +106,9 @@ def test_convert_with_dict(self): "sample": "test", "metadata": {"type": "experiment", "id": 1}, } - + csv_output = convert_form_data_to_csv(form_data) - + assert "metadata" in csv_output assert "type" in csv_output or "experiment" in csv_output @@ -124,9 +123,9 @@ def test_convert_with_none_values(self): "sample": "test", "optional_field": None, } - + csv_output = convert_form_data_to_csv(form_data) - + lines = csv_output.strip().split("\n") assert len(lines) == 2 @@ -147,18 +146,15 @@ async def test_create_dataset_success(self, mock_client_class): } mock_response.status_code = 200 mock_response.text = "" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - - result = await create_seqera_dataset( - name="test-dataset", - description="Test description" - ) - + + result = await create_seqera_dataset(name="test-dataset", description="Test description") + assert isinstance(result, DatasetCreationResult) assert result.dataset_id == "dataset_123" assert result.raw_response["dataset"]["name"] == "test-dataset" @@ -176,15 +172,15 @@ async def test_create_dataset_default_name(self, mock_client_class): } mock_response.status_code = 200 mock_response.text = "" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + result = await create_seqera_dataset() - + assert result.dataset_id == "dataset_456" # Verify a name was generated mock_client.post.assert_called_once() @@ -196,13 +192,13 @@ async def test_create_dataset_api_error(self, mock_client_class): mock_response.is_error = True mock_response.status_code = 400 mock_response.text = "Bad request" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + with pytest.raises(SeqeraServiceError, match="400"): await create_seqera_dataset(name="test") @@ -219,13 +215,13 @@ async def test_create_dataset_missing_id_in_response(self, mock_client_class): } mock_response.status_code = 200 mock_response.text = "{}" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + with pytest.raises(SeqeraServiceError, match="response lacked dataset id"): await create_seqera_dataset(name="test") @@ -241,28 +237,23 @@ async def test_upload_success(self, mock_client_class): mock_response.status_code = 200 mock_response.text = "" mock_response.json.return_value = { - "version": { - "datasetId": "dataset_789" - }, - "message": "Upload successful" + "version": {"datasetId": "dataset_789"}, + "message": "Upload successful", } - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form_data = { "sample": "test_sample", "input": "/path/file.txt", } - - result = await upload_dataset_to_seqera( - dataset_id="dataset_789", - form_data=form_data - ) - + + result = await upload_dataset_to_seqera(dataset_id="dataset_789", form_data=form_data) + assert isinstance(result, DatasetUploadResult) assert result.success is True assert result.dataset_id == "dataset_789" @@ -274,20 +265,20 @@ async def test_upload_creates_csv(self, mock_client_class): mock_response.is_error = False mock_response.status_code = 200 mock_response.text = "" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form_data = { "col1": "value1", "col2": "value2", } - + await upload_dataset_to_seqera("dataset_123", form_data) - + # Verify POST was called with files call_args = mock_client.post.call_args assert "files" in call_args[1] @@ -299,15 +290,15 @@ async def test_upload_api_error(self, mock_client_class): mock_response.is_error = True mock_response.status_code = 500 mock_response.text = "Server error" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form_data = {"sample": "test"} - + with pytest.raises(SeqeraServiceError, match="500"): await upload_dataset_to_seqera("dataset_123", form_data) @@ -318,20 +309,20 @@ async def test_upload_with_complex_data(self, mock_client_class): mock_response.is_error = False mock_response.status_code = 200 mock_response.text = "" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form_data = { "sample": "test", "files": ["file1.txt", "file2.txt"], "count": 42, "metadata": {"type": "test"}, } - + result = await upload_dataset_to_seqera("dataset_123", form_data) - + assert result.success is True diff --git a/tests/test_services_seqera.py b/tests/test_services_seqera.py index 5c92a71..4e99ba0 100644 --- a/tests/test_services_seqera.py +++ b/tests/test_services_seqera.py @@ -1,4 +1,5 @@ """Tests for Seqera service.""" + from __future__ import annotations import os @@ -6,6 +7,7 @@ import pytest +from app.schemas.workflows import WorkflowLaunchForm from app.services.seqera import ( SeqeraConfigurationError, SeqeraLaunchResult, @@ -13,7 +15,6 @@ _get_required_env, launch_seqera_workflow, ) -from app.schemas.workflows import WorkflowLaunchForm class TestGetRequiredEnv: @@ -43,26 +44,26 @@ async def test_launch_success_minimal(self, mock_client_class): "workflowId": "wf_test_123", } mock_response.reason_phrase = "OK" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + # Create form form = WorkflowLaunchForm( pipeline="https://github.com/test/repo", ) - + # Execute result = await launch_seqera_workflow(form) - + # Verify assert isinstance(result, SeqeraLaunchResult) assert result.workflow_id == "wf_test_123" assert result.status == "submitted" - + # Verify API call mock_client.post.assert_called_once() call_args = mock_client.post.call_args @@ -77,13 +78,13 @@ async def test_launch_success_with_all_params(self, mock_client_class): "workflowId": "wf_full_456", } mock_response.reason_phrase = "OK" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form = WorkflowLaunchForm( pipeline="https://github.com/test/repo", revision="main", @@ -91,11 +92,11 @@ async def test_launch_success_with_all_params(self, mock_client_class): configProfiles=["docker", "test"], paramsText="custom_param: value", ) - + result = await launch_seqera_workflow(form, dataset_id="dataset_789") - + assert result.workflow_id == "wf_full_456" - + # Verify the payload includes dataset call_args = mock_client.post.call_args payload = call_args[1]["json"] @@ -109,24 +110,24 @@ async def test_launch_includes_default_params(self, mock_client_class): mock_response.is_error = False mock_response.json.return_value = {"workflowId": "wf_123"} mock_response.reason_phrase = "OK" - + mock_client = MagicMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - + await launch_seqera_workflow(form) - + call_args = mock_client.post.call_args payload = call_args[1]["json"] params_text = payload["launch"]["paramsText"] - + # Check default params are included assert "use_dgxa100: false" in params_text - assert "project: \"za08\"" in params_text + assert 'project: "za08"' in params_text assert "outdir:" in params_text @patch("app.services.seqera.httpx.AsyncClient") @@ -136,21 +137,21 @@ async def test_launch_with_dataset_adds_input_url(self, mock_client_class): mock_response.is_error = False mock_response.json.return_value = {"workflowId": "wf_dataset_999"} mock_response.reason_phrase = "OK" - + mock_client = MagicMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - + await launch_seqera_workflow(form, dataset_id="ds_abc") - + call_args = mock_client.post.call_args payload = call_args[1]["json"] params_text = payload["launch"]["paramsText"] - + assert "input:" in params_text assert "ds_abc" in params_text assert "samplesheet.csv" in params_text @@ -163,15 +164,15 @@ async def test_launch_api_error_response(self, mock_client_class): mock_response.status_code = 400 mock_response.text = "Invalid request" mock_response.reason_phrase = "Bad Request" - + mock_client = AsyncMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - + with pytest.raises(SeqeraServiceError, match="400"): await launch_seqera_workflow(form) @@ -182,26 +183,27 @@ async def test_launch_missing_workflow_id_in_response(self, mock_client_class): mock_response.is_error = False mock_response.json.return_value = {"status": "success"} mock_response.reason_phrase = "OK" - + mock_client = MagicMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - + with pytest.raises(SeqeraServiceError, match="workflowId"): await launch_seqera_workflow(form) def test_launch_missing_env_vars(self): """Test that missing environment variables raise error.""" form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - + with patch.dict(os.environ, {}, clear=True): with pytest.raises(SeqeraConfigurationError): # This will fail synchronously when trying to get env vars import asyncio + asyncio.run(launch_seqera_workflow(form)) @patch("app.services.seqera.httpx.AsyncClient") @@ -211,24 +213,24 @@ async def test_launch_with_custom_params_text(self, mock_client_class): mock_response.is_error = False mock_response.json.return_value = {"workflowId": "wf_params_xyz"} mock_response.reason_phrase = "OK" - + mock_client = MagicMock() mock_client.post = AsyncMock(return_value=mock_response) mock_client.__aenter__ = AsyncMock(return_value=mock_client) mock_client.__aexit__ = AsyncMock(return_value=None) mock_client_class.return_value = mock_client - + form = WorkflowLaunchForm( pipeline="https://github.com/test/repo", - paramsText="my_custom_param: 42\nanother_param: test" + paramsText="my_custom_param: 42\nanother_param: test", ) - + await launch_seqera_workflow(form) - + call_args = mock_client.post.call_args payload = call_args[1]["json"] params_text = payload["launch"]["paramsText"] - + # Should contain both default and custom params assert "use_dgxa100: false" in params_text # default assert "my_custom_param: 42" in params_text # custom From 367763c7692d800623b3d9c5e923c59a322d5c18 Mon Sep 17 00:00:00 2001 From: laclac102 Date: Thu, 18 Dec 2025 14:05:17 +1100 Subject: [PATCH 05/15] fix: improve lint --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ddc0832..e7630e2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,7 +24,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -r requirements.txt - python -m pip install ruff black mypy types-httpx + python -m pip install ruff black mypy - name: Run Ruff run: ruff check app tests From ddb6e04ca8a1f016691c609f5ab45ba9b79cadaa Mon Sep 17 00:00:00 2001 From: Anne Phan <53896516+vtnphan@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:55:07 +1100 Subject: [PATCH 06/15] fix: update requirements-dev.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9057fbb..306f5fd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ # Development dependencies -pytest==8.0.0 +pytest==8.3.3 pytest-asyncio==0.23.5 pytest-cov==4.1.0 pytest-mock==3.12.0 From a0bb969aff2f15fe16150f9747f90a1c0e77c316 Mon Sep 17 00:00:00 2001 From: Anne Phan <53896516+vtnphan@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:55:22 +1100 Subject: [PATCH 07/15] fix: update .pre-commit-config.yaml Co-authored-by: Copilot <175728472+Copilot@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 a55f077..883229b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ # Pre-commit hooks configuration repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer From d6fa9f28ea32b77ffe98e151afa2dfa754cf92cf Mon Sep 17 00:00:00 2001 From: Anne Phan <53896516+vtnphan@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:55:37 +1100 Subject: [PATCH 08/15] fix: update requirements-dev.txt Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 306f5fd..8eb411b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,5 +3,5 @@ pytest==8.3.3 pytest-asyncio==0.23.5 pytest-cov==4.1.0 pytest-mock==3.12.0 -httpx==0.27.0 +httpx coverage[toml]==7.4.1 From a51ac2da178307ac78d869659f2690b0f398b186 Mon Sep 17 00:00:00 2001 From: Anne Phan <53896516+vtnphan@users.noreply.github.com> Date: Thu, 18 Dec 2025 15:05:55 +1100 Subject: [PATCH 09/15] fix: update readme Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d6bfa1c..1aac502 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ View HTML coverage report: ```bash open htmlcov/index.html # macOS xdg-open htmlcov/index.html # Linux +start htmlcov/index.html # Windows (Command Prompt / PowerShell) ``` ## Linting and Code Quality From b8c573f9ba504fd8b136fd8ec3919b5fd611eb06 Mon Sep 17 00:00:00 2001 From: Anne Phan <53896516+vtnphan@users.noreply.github.com> Date: Thu, 18 Dec 2025 15:06:27 +1100 Subject: [PATCH 10/15] fix: update .github/workflows/test-coverage.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/test-coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index c6e4c8e..17d6261 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -2,9 +2,9 @@ name: Coverage on: push: - branches: ["main", "workflows"] + branches: ["main"] pull_request: - branches: ["main", "workflows"] + branches: ["main"] jobs: tests: From d5a32095bf04c30dff5c800c6f9878934786ea21 Mon Sep 17 00:00:00 2001 From: Anne Phan <53896516+vtnphan@users.noreply.github.com> Date: Wed, 7 Jan 2026 08:14:09 +1100 Subject: [PATCH 11/15] fix: update .github/workflows/lint.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e7630e2..1bac9c0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,9 +2,9 @@ name: Lint on: push: - branches: [main, workflows] + branches: [main] pull_request: - branches: [main, workflows] + branches: [main] jobs: ruff: From ef500d7bf45b0270401c0cb02354dcf6357f98bd Mon Sep 17 00:00:00 2001 From: laclac102 Date: Wed, 7 Jan 2026 09:32:05 +1100 Subject: [PATCH 12/15] fix: improve tests --- .github/workflows/lint.yml | 2 - .gitignore | 35 +- pyproject.toml | 26 + requirements-dev.txt | 7 - requirements.txt | 5 - tests/conftest.py | 112 ++- tests/test_additional_coverage.py | 92 ++- tests/test_routes_workflows.py | 326 ++++---- tests/test_schemas.py | 401 +++++----- tests/test_services_datasets.py | 576 +++++++------- tests/test_services_seqera.py | 368 ++++----- uv.lock | 1200 +++++++++++++++++++++++++++++ 12 files changed, 2178 insertions(+), 972 deletions(-) delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt create mode 100644 uv.lock diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1bac9c0..db2fc08 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,8 +24,6 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -r requirements.txt - python -m pip install ruff black mypy - - name: Run Ruff run: ruff check app tests diff --git a/.gitignore b/.gitignore index b6657ab..9a26805 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,41 @@ +# Python .venv/ +venv/ +env/ __pycache__/ *.py[cod] +*$py.class +*.so +*.egg +*.egg-info/ +dist/ +build/ +*.whl + +# Environment .env -*.log +.env.local +.env.*.local + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.hypothesis/ + +# IDE .DS_Store .idea/ .vscode/ +*.swp +*.swo +*~ + +# Logs +*.log +logs/ + +# UV +# Note: uv.lock is tracked for reproducibility +# .python-version is tracked if using specific Python version diff --git a/pyproject.toml b/pyproject.toml index 3ef09d8..7641a40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,26 @@ name = "sbp-backend" version = "1.0.0" description = "Structural Biology Platform Backend API" requires-python = ">=3.10" +dependencies = [ + "fastapi~=0.111", + "uvicorn[standard]~=0.29", + "python-dotenv~=1.0", + "httpx~=0.27", + "pydantic~=2.7", +] + +[project.optional-dependencies] +dev = [ + "pytest~=8.3", + "pytest-asyncio~=0.23", + "pytest-cov~=4.1", + "pytest-mock~=3.12", + "httpx~=0.28", + "coverage[toml]~=7.4", + "ruff~=0.14", + "black~=23.3", + "mypy~=1.19", +] [tool.pytest.ini_options] asyncio_mode = "auto" @@ -93,3 +113,9 @@ warn_return_any = true warn_unused_configs = true disallow_untyped_defs = false ignore_missing_imports = true + +[dependency-groups] +dev = [ + "polyfactory~=2.16", + "respx~=0.21", +] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 8eb411b..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Development dependencies -pytest==8.3.3 -pytest-asyncio==0.23.5 -pytest-cov==4.1.0 -pytest-mock==3.12.0 -httpx -coverage[toml]==7.4.1 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8db1b8b..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -fastapi==0.111.0 -uvicorn[standard]==0.29.0 -python-dotenv==1.0.1 -httpx==0.27.0 -pydantic==2.7.1 diff --git a/tests/conftest.py b/tests/conftest.py index 378c89d..657f1f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,11 +4,12 @@ import os from collections.abc import AsyncGenerator, Generator -from unittest.mock import AsyncMock, MagicMock import pytest +import respx from fastapi.testclient import TestClient from httpx import AsyncClient +from polyfactory.factories.pydantic_factory import ModelFactory # Set test environment variables before importing app os.environ["ALLOWED_ORIGINS"] = "http://localhost:3000,http://localhost:4200" @@ -19,6 +20,74 @@ os.environ["WORK_DIR"] = "/test/work/dir" from app.main import create_app +from app.schemas.workflows import ( + LaunchDetails, + LaunchLogs, + ListRunsResponse, + RunInfo, + WorkflowLaunchForm, + WorkflowLaunchPayload, + WorkflowLaunchResponse, +) + + +# ============================================================================ +# Polyfactory Factories - Auto-generate test data from Pydantic schemas +# ============================================================================ + + +class WorkflowLaunchFormFactory(ModelFactory[WorkflowLaunchForm]): + """Factory for generating WorkflowLaunchForm test data.""" + + __model__ = WorkflowLaunchForm + __check_model__ = False + + +class WorkflowLaunchPayloadFactory(ModelFactory[WorkflowLaunchPayload]): + """Factory for generating WorkflowLaunchPayload test data.""" + + __model__ = WorkflowLaunchPayload + __check_model__ = False + + +class WorkflowLaunchResponseFactory(ModelFactory[WorkflowLaunchResponse]): + """Factory for generating WorkflowLaunchResponse test data.""" + + __model__ = WorkflowLaunchResponse + __check_model__ = False + + +class RunInfoFactory(ModelFactory[RunInfo]): + """Factory for generating RunInfo test data.""" + + __model__ = RunInfo + __check_model__ = False + + +class ListRunsResponseFactory(ModelFactory[ListRunsResponse]): + """Factory for generating ListRunsResponse test data.""" + + __model__ = ListRunsResponse + __check_model__ = False + + +class LaunchLogsFactory(ModelFactory[LaunchLogs]): + """Factory for generating LaunchLogs test data.""" + + __model__ = LaunchLogs + __check_model__ = False + + +class LaunchDetailsFactory(ModelFactory[LaunchDetails]): + """Factory for generating LaunchDetails test data.""" + + __model__ = LaunchDetails + __check_model__ = False + + +# ============================================================================ +# FastAPI Test Clients +# ============================================================================ @pytest.fixture @@ -41,41 +110,20 @@ async def async_client(app) -> AsyncGenerator[AsyncClient, None]: yield ac -@pytest.fixture -def mock_httpx_response(): - """Create a mock httpx Response.""" - - def _create_response( - status_code: int = 200, - json_data: dict | None = None, - text: str = "", - is_error: bool = False, - ): - response = MagicMock() - response.status_code = status_code - response.is_error = is_error - response.text = text - if json_data: - response.json.return_value = json_data - return response - - return _create_response - - -@pytest.fixture -def mock_async_client(mock_httpx_response): - """Create a mock async HTTP client.""" - mock_client = AsyncMock() - mock_client.post = AsyncMock() - mock_client.get = AsyncMock() - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock() - return mock_client +# ============================================================================ +# Legacy Fixtures (kept for backward compatibility) +# Note: Consider using factories directly in tests instead +# ============================================================================ +# Note: respx is now used for HTTP mocking instead of manual AsyncMock +# respx automatically handles httpx.AsyncClient mocking @pytest.fixture def sample_workflow_launch_form(): - """Sample workflow launch form data.""" + """Sample workflow launch form data. + + NOTE: Consider using WorkflowLaunchFormFactory.build() directly in tests. + """ return { "pipeline": "https://github.com/nextflow-io/hello", "revision": "main", diff --git a/tests/test_additional_coverage.py b/tests/test_additional_coverage.py index 6f266a1..05bcf51 100644 --- a/tests/test_additional_coverage.py +++ b/tests/test_additional_coverage.py @@ -9,52 +9,46 @@ from app.services.datasets import DatasetUploadResult -class TestUploadDataset: - """Tests for dataset upload endpoint.""" - - @patch("app.routes.workflows.upload_dataset_to_seqera") - @patch("app.routes.workflows.create_seqera_dataset") - async def test_upload_dataset_success(self, mock_create, mock_upload): - """Test successful dataset upload.""" - # Mock dataset creation - mock_create_result = AsyncMock() - mock_create_result.dataset_id = "dataset_123" - mock_create_result.raw_response = {"id": "dataset_123"} - mock_create.return_value = mock_create_result - - # Mock upload - mock_upload_result = DatasetUploadResult( - success=True, - dataset_id="dataset_123", - message="Uploaded", - ) - mock_upload.return_value = mock_upload_result - - # Create request - request = DatasetUploadRequest( - formData={"sample": "test"}, - datasetName="test-dataset", - ) - - # Execute - response = await upload_dataset(request) - - # Verify - assert response.success is True - assert response.datasetId == "dataset_123" - mock_create.assert_called_once() - mock_upload.assert_called_once() - - -class TestGetDetails: - """Tests for get details endpoint.""" - - async def test_get_details_returns_placeholder(self): - """Test that get_details returns proper placeholder data.""" - result = await get_details("run_abc123") - - assert result.id == "run_abc123" - assert result.status == "UNKNOWN" - assert result.runName == "" - assert isinstance(result.configFiles, list) - assert isinstance(result.params, dict) +@patch("app.routes.workflows.upload_dataset_to_seqera") +@patch("app.routes.workflows.create_seqera_dataset") +async def test_upload_dataset_success(mock_create, mock_upload): + """Test successful dataset upload.""" + # Mock dataset creation + mock_create_result = AsyncMock() + mock_create_result.dataset_id = "dataset_123" + mock_create_result.raw_response = {"id": "dataset_123"} + mock_create.return_value = mock_create_result + + # Mock upload + mock_upload_result = DatasetUploadResult( + success=True, + dataset_id="dataset_123", + message="Uploaded", + ) + mock_upload.return_value = mock_upload_result + + # Create request + request = DatasetUploadRequest( + formData={"sample": "test"}, + datasetName="test-dataset", + ) + + # Execute + response = await upload_dataset(request) + + # Verify + assert response.success is True + assert response.datasetId == "dataset_123" + mock_create.assert_called_once() + mock_upload.assert_called_once() + + +async def test_get_details_returns_placeholder(): + """Test that get_details returns proper placeholder data.""" + result = await get_details("run_abc123") + + assert result.id == "run_abc123" + assert result.status == "UNKNOWN" + assert result.runName == "" + assert isinstance(result.configFiles, list) + assert isinstance(result.params, dict) diff --git a/tests/test_routes_workflows.py b/tests/test_routes_workflows.py index 81bd65a..14285e2 100644 --- a/tests/test_routes_workflows.py +++ b/tests/test_routes_workflows.py @@ -13,205 +13,197 @@ ) -class TestLaunchWorkflow: - """Tests for POST /api/workflows/launch endpoint.""" - - @patch("app.routes.workflows.launch_seqera_workflow") - async def test_launch_success_without_dataset(self, mock_launch, client: TestClient): - """Test successful workflow launch without dataset.""" - mock_launch.return_value = SeqeraLaunchResult( - workflow_id="wf_123", - status="submitted", - message="Success", - ) - - payload = { - "launch": { - "pipeline": "https://github.com/test/repo", - "runName": "test-run", - } +@patch("app.routes.workflows.launch_seqera_workflow") +async def test_launch_success_without_dataset(mock_launch, client: TestClient): + """Test successful workflow launch without dataset.""" + mock_launch.return_value = SeqeraLaunchResult( + workflow_id="wf_123", + status="submitted", + message="Success", + ) + + payload = { + "launch": { + "pipeline": "https://github.com/test/repo", + "runName": "test-run", } - - response = client.post("/api/workflows/launch", json=payload) - - assert response.status_code == 201 - data = response.json() - assert data["runId"] == "wf_123" - assert data["status"] == "submitted" - assert "submitTime" in data - - @patch("app.routes.workflows.upload_dataset_to_seqera") - @patch("app.routes.workflows.create_seqera_dataset") - @patch("app.routes.workflows.launch_seqera_workflow") - async def test_launch_success_with_form_data( - self, mock_launch, mock_create_dataset, mock_upload, client: TestClient - ): - """Test successful workflow launch with form data.""" - # Mock dataset creation - mock_create_result = AsyncMock() - mock_create_result.dataset_id = "dataset_456" - mock_create_dataset.return_value = mock_create_result - - # Mock dataset upload - mock_upload.return_value = None - - # Mock workflow launch - mock_launch.return_value = SeqeraLaunchResult( - workflow_id="wf_789", - status="submitted", - ) - - payload = { - "launch": { - "pipeline": "https://github.com/test/repo", - "runName": "test-with-data", - }, - "formData": { - "sample": "test", - "input": "/path/file.txt", - }, + } + + response = client.post("/api/workflows/launch", json=payload) + + assert response.status_code == 201 + data = response.json() + assert data["runId"] == "wf_123" + assert data["status"] == "submitted" + assert "submitTime" in data + + +@patch("app.routes.workflows.upload_dataset_to_seqera") +@patch("app.routes.workflows.create_seqera_dataset") +@patch("app.routes.workflows.launch_seqera_workflow") +async def test_launch_success_with_form_data( + mock_launch, mock_create_dataset, mock_upload, client: TestClient +): + """Test successful workflow launch with form data.""" + # Mock dataset creation + mock_create_result = AsyncMock() + mock_create_result.dataset_id = "dataset_456" + mock_create_dataset.return_value = mock_create_result + + # Mock dataset upload + mock_upload.return_value = None + + # Mock workflow launch + mock_launch.return_value = SeqeraLaunchResult( + workflow_id="wf_789", + status="submitted", + ) + + payload = { + "launch": { + "pipeline": "https://github.com/test/repo", + "runName": "test-with-data", + }, + "formData": { + "sample": "test", + "input": "/path/file.txt", + }, + } + + response = client.post("/api/workflows/launch", json=payload) + + assert response.status_code == 201 + data = response.json() + assert data["runId"] == "wf_789" + + # Verify dataset creation was called + mock_create_dataset.assert_called_once() + mock_upload.assert_called_once() + + +@patch("app.routes.workflows.launch_seqera_workflow") +async def test_launch_configuration_error(mock_launch, client: TestClient): + """Test launch with configuration error.""" + mock_launch.side_effect = SeqeraConfigurationError("Missing API token") + + payload = { + "launch": { + "pipeline": "https://github.com/test/repo", } + } - response = client.post("/api/workflows/launch", json=payload) + response = client.post("/api/workflows/launch", json=payload) - assert response.status_code == 201 - data = response.json() - assert data["runId"] == "wf_789" + assert response.status_code == 500 + assert "Missing API token" in response.json()["detail"] - # Verify dataset creation was called - mock_create_dataset.assert_called_once() - mock_upload.assert_called_once() - @patch("app.routes.workflows.launch_seqera_workflow") - async def test_launch_configuration_error(self, mock_launch, client: TestClient): - """Test launch with configuration error.""" - mock_launch.side_effect = SeqeraConfigurationError("Missing API token") +@patch("app.routes.workflows.launch_seqera_workflow") +async def test_launch_service_error(mock_launch, client: TestClient): + """Test launch with Seqera service error.""" + mock_launch.side_effect = SeqeraServiceError("API returned 502") - payload = { - "launch": { - "pipeline": "https://github.com/test/repo", - } + payload = { + "launch": { + "pipeline": "https://github.com/test/repo", } + } - response = client.post("/api/workflows/launch", json=payload) - - assert response.status_code == 500 - assert "Missing API token" in response.json()["detail"] - - @patch("app.routes.workflows.launch_seqera_workflow") - async def test_launch_service_error(self, mock_launch, client: TestClient): - """Test launch with Seqera service error.""" - mock_launch.side_effect = SeqeraServiceError("API returned 502") + response = client.post("/api/workflows/launch", json=payload) - payload = { - "launch": { - "pipeline": "https://github.com/test/repo", - } - } - - response = client.post("/api/workflows/launch", json=payload) + assert response.status_code == 502 + assert "API returned 502" in response.json()["detail"] - assert response.status_code == 502 - assert "API returned 502" in response.json()["detail"] - def test_launch_invalid_payload(self, client: TestClient): - """Test launch with invalid payload.""" - payload = { - "launch": { - "pipeline": "", # Empty pipeline - } +def test_launch_invalid_payload(client: TestClient): + """Test launch with invalid payload.""" + payload = { + "launch": { + "pipeline": "", # Empty pipeline } + } - response = client.post("/api/workflows/launch", json=payload) - - assert response.status_code == 422 # Validation error + response = client.post("/api/workflows/launch", json=payload) + assert response.status_code == 422 # Validation error -class TestCancelWorkflow: - """Tests for POST /api/workflows/{run_id}/cancel endpoint.""" - def test_cancel_workflow_success(self, client: TestClient): - """Test successful workflow cancellation.""" - response = client.post("/api/workflows/run_123/cancel") +def test_cancel_workflow_success(client: TestClient): + """Test successful workflow cancellation.""" + response = client.post("/api/workflows/run_123/cancel") - assert response.status_code == 200 - data = response.json() - assert data["runId"] == "run_123" - assert data["status"] == "cancelled" - assert "message" in data + assert response.status_code == 200 + data = response.json() + assert data["runId"] == "run_123" + assert data["status"] == "cancelled" + assert "message" in data -class TestListRuns: - """Tests for GET /api/workflows/runs endpoint.""" +def test_list_runs_default_params(client: TestClient): + """Test listing runs with default parameters.""" + response = client.get("/api/workflows/runs") - def test_list_runs_default_params(self, client: TestClient): - """Test listing runs with default parameters.""" - response = client.get("/api/workflows/runs") + assert response.status_code == 200 + data = response.json() + assert "runs" in data + assert data["limit"] == 50 + assert data["offset"] == 0 + assert data["total"] == 0 - assert response.status_code == 200 - data = response.json() - assert "runs" in data - assert data["limit"] == 50 - assert data["offset"] == 0 - assert data["total"] == 0 - def test_list_runs_with_filters(self, client: TestClient): - """Test listing runs with filter parameters.""" - response = client.get( - "/api/workflows/runs", - params={ - "status": "running", - "workspace": "test_ws", - "limit": 10, - "offset": 5, - }, - ) +def test_list_runs_with_filters(client: TestClient): + """Test listing runs with filter parameters.""" + response = client.get( + "/api/workflows/runs", + params={ + "status": "running", + "workspace": "test_ws", + "limit": 10, + "offset": 5, + }, + ) - assert response.status_code == 200 - data = response.json() - assert data["limit"] == 10 - assert data["offset"] == 5 + assert response.status_code == 200 + data = response.json() + assert data["limit"] == 10 + assert data["offset"] == 5 - def test_list_runs_limit_validation(self, client: TestClient): - """Test that limit must be between 1 and 200.""" - # Test limit too high - response = client.get("/api/workflows/runs", params={"limit": 300}) - assert response.status_code == 422 - # Test limit too low - response = client.get("/api/workflows/runs", params={"limit": 0}) - assert response.status_code == 422 +def test_list_runs_limit_validation(client: TestClient): + """Test that limit must be between 1 and 200.""" + # Test limit too high + response = client.get("/api/workflows/runs", params={"limit": 300}) + assert response.status_code == 422 - def test_list_runs_offset_validation(self, client: TestClient): - """Test that offset must be non-negative.""" - response = client.get("/api/workflows/runs", params={"offset": -1}) - assert response.status_code == 422 + # Test limit too low + response = client.get("/api/workflows/runs", params={"limit": 0}) + assert response.status_code == 422 -class TestGetLogs: - """Tests for GET /api/workflows/{run_id}/logs endpoint.""" +def test_list_runs_offset_validation(client: TestClient): + """Test that offset must be non-negative.""" + response = client.get("/api/workflows/runs", params={"offset": -1}) + assert response.status_code == 422 - def test_get_logs_success(self, client: TestClient): - """Test successful log retrieval.""" - response = client.get("/api/workflows/run_123/logs") - assert response.status_code == 200 - data = response.json() - assert "entries" in data - assert "truncated" in data - assert "pending" in data - assert isinstance(data["entries"], list) +def test_get_logs_success(client: TestClient): + """Test successful log retrieval.""" + response = client.get("/api/workflows/run_123/logs") + assert response.status_code == 200 + data = response.json() + assert "entries" in data + assert "truncated" in data + assert "pending" in data + assert isinstance(data["entries"], list) -class TestGetDetails: - """Tests for GET /api/workflows/{run_id}/details endpoint.""" - def test_get_details_success(self, client: TestClient): - """Test successful details retrieval.""" - response = client.get("/api/workflows/run_123/details") +def test_get_details_success(client: TestClient): + """Test successful details retrieval.""" + response = client.get("/api/workflows/run_123/details") - assert response.status_code == 200 - data = response.json() - assert data["id"] == "run_123" - assert "status" in data - assert "runName" in data + assert response.status_code == 200 + data = response.json() + assert data["id"] == "run_123" + assert "status" in data + assert "runName" in data diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 0310e64..750b805 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -19,238 +19,223 @@ ) -class TestWorkflowLaunchForm: - """Tests for WorkflowLaunchForm schema.""" - - def test_valid_minimal_form(self): - """Test WorkflowLaunchForm with minimal valid data.""" - form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - - assert form.pipeline == "https://github.com/test/repo" - assert form.revision is None - assert form.configProfiles == [] - assert form.runName is None - assert form.paramsText is None - - def test_valid_complete_form(self): - """Test WorkflowLaunchForm with all fields.""" - form = WorkflowLaunchForm( - pipeline="https://github.com/test/repo", - revision="main", - configProfiles=["docker", "test"], - runName="my-test-run", - paramsText="param1: value1\nparam2: value2", - ) - - assert form.pipeline == "https://github.com/test/repo" - assert form.revision == "main" - assert form.configProfiles == ["docker", "test"] - assert form.runName == "my-test-run" - assert "param1" in form.paramsText - - def test_pipeline_required(self): - """Test that pipeline field is required.""" - with pytest.raises(ValidationError) as exc_info: - WorkflowLaunchForm() - - errors = exc_info.value.errors() - assert any(error["loc"] == ("pipeline",) for error in errors) - - def test_pipeline_cannot_be_empty(self): - """Test that pipeline cannot be empty string.""" - with pytest.raises(ValidationError, match="pipeline is required"): - WorkflowLaunchForm(pipeline="") - - def test_pipeline_whitespace_stripped(self): - """Test that pipeline whitespace is stripped.""" - form = WorkflowLaunchForm(pipeline=" https://github.com/test/repo ") - assert form.pipeline == "https://github.com/test/repo" - - def test_extra_fields_forbidden(self): - """Test that extra fields are not allowed.""" - with pytest.raises(ValidationError): - WorkflowLaunchForm(pipeline="https://github.com/test/repo", extraField="not allowed") - - -class TestWorkflowLaunchPayload: - """Tests for WorkflowLaunchPayload schema.""" - - def test_valid_payload_with_launch_only(self): - """Test payload with only launch data.""" - payload = WorkflowLaunchPayload(launch={"pipeline": "https://github.com/test/repo"}) +def test_valid_minimal_form(): + """Test WorkflowLaunchForm with minimal valid data.""" + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - assert payload.launch.pipeline == "https://github.com/test/repo" - assert payload.datasetId is None - assert payload.formData is None + assert form.pipeline == "https://github.com/test/repo" + assert form.revision is None + assert form.configProfiles == [] + assert form.runName is None + assert form.paramsText is None - def test_valid_payload_with_dataset_id(self): - """Test payload with dataset ID.""" - payload = WorkflowLaunchPayload( - launch={"pipeline": "https://github.com/test/repo"}, - datasetId="dataset_123", - ) - assert payload.datasetId == "dataset_123" - - def test_valid_payload_with_form_data(self): - """Test payload with form data.""" - form_data = { - "sample": "test", - "input": "/path/to/file", - "param": 42, - } - payload = WorkflowLaunchPayload( - launch={"pipeline": "https://github.com/test/repo"}, - formData=form_data, - ) +def test_valid_complete_form(): + """Test WorkflowLaunchForm with all fields.""" + form = WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + revision="main", + configProfiles=["docker", "test"], + runName="my-test-run", + paramsText="param1: value1\nparam2: value2", + ) - assert payload.formData == form_data + assert form.pipeline == "https://github.com/test/repo" + assert form.revision == "main" + assert form.configProfiles == ["docker", "test"] + assert form.runName == "my-test-run" + assert "param1" in form.paramsText - def test_extra_fields_forbidden(self): - """Test that extra fields are not allowed.""" - with pytest.raises(ValidationError): - WorkflowLaunchPayload( - launch={"pipeline": "https://github.com/test/repo"}, unknownField="value" - ) +def test_pipeline_required(): + """Test that pipeline field is required.""" + with pytest.raises(ValidationError) as exc_info: + WorkflowLaunchForm() -class TestWorkflowLaunchResponse: - """Tests for WorkflowLaunchResponse schema.""" + errors = exc_info.value.errors() + assert any(error["loc"] == ("pipeline",) for error in errors) - def test_valid_response(self): - """Test creating a valid launch response.""" - response = WorkflowLaunchResponse( - message="Workflow launched", - runId="run_123", - status="submitted", - submitTime=datetime(2024, 1, 1, 12, 0, 0), - ) - assert response.message == "Workflow launched" - assert response.runId == "run_123" - assert response.status == "submitted" - assert response.submitTime.year == 2024 +def test_pipeline_cannot_be_empty(): + """Test that pipeline cannot be empty string.""" + with pytest.raises(ValidationError, match="pipeline is required"): + WorkflowLaunchForm(pipeline="") -class TestCancelWorkflowResponse: - """Tests for CancelWorkflowResponse schema.""" - - def test_valid_cancel_response(self): - """Test creating a valid cancel response.""" - response = CancelWorkflowResponse( - message="Cancelled", - runId="run_123", - status="cancelled", - ) +def test_pipeline_whitespace_stripped(): + """Test that pipeline whitespace is stripped.""" + form = WorkflowLaunchForm(pipeline=" https://github.com/test/repo ") + assert form.pipeline == "https://github.com/test/repo" - assert response.message == "Cancelled" - assert response.runId == "run_123" - assert response.status == "cancelled" +def test_extra_fields_forbidden(): + """Test that extra fields are not allowed.""" + with pytest.raises(ValidationError): + WorkflowLaunchForm(pipeline="https://github.com/test/repo", extraField="not allowed") -class TestRunInfo: - """Tests for RunInfo schema.""" - def test_valid_run_info(self): - """Test creating valid run info.""" - run_info = RunInfo( - id="run_123", - run="test-run", - workflow="test-workflow", - status="running", - date="2024-01-01", - cancel="false", - ) +def test_valid_payload_with_launch_only(): + """Test payload with only launch data.""" + payload = WorkflowLaunchPayload(launch={"pipeline": "https://github.com/test/repo"}) - assert run_info.id == "run_123" - assert run_info.status == "running" + assert payload.launch.pipeline == "https://github.com/test/repo" + assert payload.datasetId is None + assert payload.formData is None -class TestListRunsResponse: - """Tests for ListRunsResponse schema.""" +def test_valid_payload_with_dataset_id(): + """Test payload with dataset ID.""" + payload = WorkflowLaunchPayload( + launch={"pipeline": "https://github.com/test/repo"}, + datasetId="dataset_123", + ) - def test_empty_runs_list(self): - """Test response with empty runs list.""" - response = ListRunsResponse( - runs=[], - total=0, - limit=50, - offset=0, - ) + assert payload.datasetId == "dataset_123" - assert response.runs == [] - assert response.total == 0 - - def test_runs_list_with_data(self): - """Test response with run data.""" - run_info = RunInfo( - id="run_123", - run="test", - workflow="wf", - status="done", - date="2024-01-01", - cancel="false", - ) - response = ListRunsResponse( - runs=[run_info], - total=1, - limit=50, - offset=0, - ) - assert len(response.runs) == 1 - assert response.total == 1 +def test_valid_payload_with_form_data(): + """Test payload with form data.""" + form_data = { + "sample": "test", + "input": "/path/to/file", + "param": 42, + } + payload = WorkflowLaunchPayload( + launch={"pipeline": "https://github.com/test/repo"}, + formData=form_data, + ) + assert payload.formData == form_data -class TestLaunchLogs: - """Tests for LaunchLogs schema.""" - def test_valid_logs(self): - """Test creating valid launch logs.""" - logs = LaunchLogs( - truncated=False, - entries=["log line 1", "log line 2"], - rewindToken="token1", - forwardToken="token2", - pending=False, - message="Logs retrieved", +def test_payload_extra_fields_forbidden(): + """Test that extra fields are not allowed in payload.""" + with pytest.raises(ValidationError): + WorkflowLaunchPayload( + launch={"pipeline": "https://github.com/test/repo"}, unknownField="value" ) - assert len(logs.entries) == 2 - assert logs.truncated is False - - -class TestLaunchDetails: - """Tests for LaunchDetails schema.""" - - def test_valid_details(self): - """Test creating valid launch details.""" - details = LaunchDetails( - requiresAttention=False, - status="completed", - ownerId=123, - repository="https://github.com/test/repo", - id="launch_123", - submit="2024-01-01T12:00:00", - start="2024-01-01T12:01:00", - complete="2024-01-01T12:10:00", - dateCreated="2024-01-01T12:00:00", - lastUpdated="2024-01-01T12:10:00", - runName="test-run", - sessionId="session_123", - profile="standard", - workDir="/work", - commitId="abc123", - userName="testuser", - scriptId="script_123", - revision="main", - commandLine="nextflow run", - projectName="test-project", - scriptName="main.nf", - launchId="launch_123", - configFiles=["nextflow.config"], - params={"test": "value"}, - ) - assert details.status == "completed" - assert details.ownerId == 123 +def test_valid_response(): + """Test creating a valid launch response.""" + response = WorkflowLaunchResponse( + message="Workflow launched", + runId="run_123", + status="submitted", + submitTime=datetime(2024, 1, 1, 12, 0, 0), + ) + + assert response.message == "Workflow launched" + assert response.runId == "run_123" + assert response.status == "submitted" + assert response.submitTime.year == 2024 + + +def test_valid_cancel_response(): + """Test creating a valid cancel response.""" + response = CancelWorkflowResponse( + message="Cancelled", + runId="run_123", + status="cancelled", + ) + + assert response.message == "Cancelled" + assert response.runId == "run_123" + assert response.status == "cancelled" + + +def test_valid_run_info(): + """Test creating valid run info.""" + run_info = RunInfo( + id="run_123", + run="test-run", + workflow="test-workflow", + status="running", + date="2024-01-01", + cancel="false", + ) + + assert run_info.id == "run_123" + assert run_info.status == "running" + + +def test_empty_runs_list(): + """Test response with empty runs list.""" + response = ListRunsResponse( + runs=[], + total=0, + limit=50, + offset=0, + ) + + assert response.runs == [] + assert response.total == 0 + + +def test_runs_list_with_data(): + """Test response with run data.""" + run_info = RunInfo( + id="run_123", + run="test", + workflow="wf", + status="done", + date="2024-01-01", + cancel="false", + ) + response = ListRunsResponse( + runs=[run_info], + total=1, + limit=50, + offset=0, + ) + + assert len(response.runs) == 1 + assert response.total == 1 + + +def test_valid_logs(): + """Test creating valid launch logs.""" + logs = LaunchLogs( + truncated=False, + entries=["log line 1", "log line 2"], + rewindToken="token1", + forwardToken="token2", + pending=False, + message="Logs retrieved", + ) + + assert len(logs.entries) == 2 + assert logs.truncated is False + + +def test_valid_details(): + """Test creating valid launch details.""" + details = LaunchDetails( + requiresAttention=False, + status="completed", + ownerId=123, + repository="https://github.com/test/repo", + id="launch_123", + submit="2024-01-01T12:00:00", + start="2024-01-01T12:01:00", + complete="2024-01-01T12:10:00", + dateCreated="2024-01-01T12:00:00", + lastUpdated="2024-01-01T12:10:00", + runName="test-run", + sessionId="session_123", + profile="standard", + workDir="/work", + commitId="abc123", + userName="testuser", + scriptId="script_123", + revision="main", + commandLine="nextflow run", + projectName="test-project", + scriptName="main.nf", + launchId="launch_123", + configFiles=["nextflow.config"], + params={"test": "value"}, + ) + + assert details.status == "completed" + assert details.ownerId == 123 diff --git a/tests/test_services_datasets.py b/tests/test_services_datasets.py index d16df22..26797cb 100644 --- a/tests/test_services_datasets.py +++ b/tests/test_services_datasets.py @@ -3,9 +3,10 @@ from __future__ import annotations import json -from unittest.mock import AsyncMock, MagicMock, patch +import httpx import pytest +import respx from app.services.datasets import ( DatasetCreationResult, @@ -18,311 +19,280 @@ ) -class TestStringifyField: - """Tests for _stringify_field helper.""" - - def test_stringify_none(self): - """Test stringifying None returns empty string.""" - assert _stringify_field(None) == "" - - def test_stringify_string(self): - """Test stringifying a string.""" - assert _stringify_field("hello") == "hello" +def test_stringify_none(): + """Test stringifying None returns empty string.""" + assert _stringify_field(None) == "" - def test_stringify_number(self): - """Test stringifying a number.""" - assert _stringify_field(42) == "42" - assert _stringify_field(3.14) == "3.14" - def test_stringify_list(self): - """Test stringifying a list.""" - assert _stringify_field(["a", "b", "c"]) == "a;b;c" +def test_stringify_string(): + """Test stringifying a string.""" + assert _stringify_field("hello") == "hello" - def test_stringify_list_with_none(self): - """Test stringifying a list containing None.""" - assert _stringify_field(["a", None, "c"]) == "a;;c" - - def test_stringify_dict(self): - """Test stringifying a dict as JSON.""" - result = _stringify_field({"key": "value", "num": 42}) - parsed = json.loads(result) - assert parsed["key"] == "value" - assert parsed["num"] == 42 - - def test_stringify_boolean(self): - """Test stringifying boolean.""" - assert _stringify_field(True) == "True" - assert _stringify_field(False) == "False" - - -class TestConvertFormDataToCsv: - """Tests for convert_form_data_to_csv function.""" - - def test_convert_simple_data(self): - """Test converting simple form data to CSV.""" - form_data = { - "name": "test", - "value": "123", - "flag": "true", - } - - csv_output = convert_form_data_to_csv(form_data) - - lines = csv_output.strip().split("\n") - assert len(lines) == 2 # header + 1 data row - assert "name" in lines[0] - assert "value" in lines[0] - assert "flag" in lines[0] - assert "test" in lines[1] - assert "123" in lines[1] - - def test_convert_with_numbers(self): - """Test converting data with numeric values.""" - form_data = { - "sample_id": "sample_001", - "count": 42, - "ratio": 3.14, - } - - csv_output = convert_form_data_to_csv(form_data) - - assert "42" in csv_output - assert "3.14" in csv_output - - def test_convert_with_list(self): - """Test converting data with list values.""" - form_data = { - "sample": "test", - "files": ["file1.txt", "file2.txt"], - } - - csv_output = convert_form_data_to_csv(form_data) - - assert "file1.txt;file2.txt" in csv_output - - def test_convert_with_dict(self): - """Test converting data with dict values.""" - form_data = { - "sample": "test", - "metadata": {"type": "experiment", "id": 1}, - } - - csv_output = convert_form_data_to_csv(form_data) - - assert "metadata" in csv_output - assert "type" in csv_output or "experiment" in csv_output - - def test_convert_empty_data_raises_error(self): - """Test that empty form data raises ValueError.""" - with pytest.raises(ValueError, match="formData cannot be empty"): - convert_form_data_to_csv({}) - - def test_convert_with_none_values(self): - """Test converting data with None values.""" - form_data = { - "sample": "test", - "optional_field": None, - } - - csv_output = convert_form_data_to_csv(form_data) - - lines = csv_output.strip().split("\n") - assert len(lines) == 2 - - -class TestCreateSeqeraDataset: - """Tests for create_seqera_dataset function.""" - - @patch("app.services.datasets.httpx.AsyncClient") - async def test_create_dataset_success(self, mock_client_class): - """Test successful dataset creation.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = { - "dataset": { - "id": "dataset_123", - "name": "test-dataset", - } - } - mock_response.status_code = 200 - mock_response.text = "" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - result = await create_seqera_dataset(name="test-dataset", description="Test description") - - assert isinstance(result, DatasetCreationResult) - assert result.dataset_id == "dataset_123" - assert result.raw_response["dataset"]["name"] == "test-dataset" - - @patch("app.services.datasets.httpx.AsyncClient") - async def test_create_dataset_default_name(self, mock_client_class): - """Test dataset creation with auto-generated name.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = { - "dataset": { - "id": "dataset_456", - "name": "dataset-1234567890", - } - } - mock_response.status_code = 200 - mock_response.text = "" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - result = await create_seqera_dataset() - - assert result.dataset_id == "dataset_456" - # Verify a name was generated - mock_client.post.assert_called_once() - - @patch("app.services.datasets.httpx.AsyncClient") - async def test_create_dataset_api_error(self, mock_client_class): - """Test dataset creation with API error.""" - mock_response = AsyncMock() - mock_response.is_error = True - mock_response.status_code = 400 - mock_response.text = "Bad request" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - with pytest.raises(SeqeraServiceError, match="400"): - await create_seqera_dataset(name="test") - - @patch("app.services.datasets.httpx.AsyncClient") - async def test_create_dataset_missing_id_in_response(self, mock_client_class): - """Test handling when response is missing dataset ID.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = { - "dataset": { - "name": "test-dataset", - # Missing "id" field - } - } - mock_response.status_code = 200 - mock_response.text = "{}" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - with pytest.raises(SeqeraServiceError, match="response lacked dataset id"): - await create_seqera_dataset(name="test") - - -class TestUploadDatasetToSeqera: - """Tests for upload_dataset_to_seqera function.""" - - @patch("app.services.datasets.httpx.AsyncClient") - async def test_upload_success(self, mock_client_class): - """Test successful dataset upload.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.status_code = 200 - mock_response.text = "" - mock_response.json.return_value = { - "version": {"datasetId": "dataset_789"}, - "message": "Upload successful", - } - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form_data = { - "sample": "test_sample", - "input": "/path/file.txt", - } - - result = await upload_dataset_to_seqera(dataset_id="dataset_789", form_data=form_data) - - assert isinstance(result, DatasetUploadResult) - assert result.success is True - assert result.dataset_id == "dataset_789" - - @patch("app.services.datasets.httpx.AsyncClient") - async def test_upload_creates_csv(self, mock_client_class): - """Test that upload creates proper CSV.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.status_code = 200 - mock_response.text = "" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form_data = { - "col1": "value1", - "col2": "value2", - } +def test_stringify_number(): + """Test stringifying a number.""" + assert _stringify_field(42) == "42" + assert _stringify_field(3.14) == "3.14" + + +def test_stringify_list(): + """Test stringifying a list.""" + assert _stringify_field(["a", "b", "c"]) == "a;b;c" + + +def test_stringify_list_with_none(): + """Test stringifying a list containing None.""" + assert _stringify_field(["a", None, "c"]) == "a;;c" + + +def test_stringify_dict(): + """Test stringifying a dict as JSON.""" + result = _stringify_field({"key": "value", "num": 42}) + parsed = json.loads(result) + assert parsed["key"] == "value" + assert parsed["num"] == 42 + + +def test_stringify_boolean(): + """Test stringifying boolean.""" + assert _stringify_field(True) == "True" + assert _stringify_field(False) == "False" + + +def test_convert_simple_data(): + """Test converting simple form data to CSV.""" + form_data = { + "name": "test", + "value": "123", + "flag": "true", + } + + csv_output = convert_form_data_to_csv(form_data) + + lines = csv_output.strip().split("\n") + assert len(lines) == 2 # header + 1 data row + assert "name" in lines[0] + assert "value" in lines[0] + assert "flag" in lines[0] + assert "test" in lines[1] + assert "123" in lines[1] + + +def test_convert_with_numbers(): + """Test converting data with numeric values.""" + form_data = { + "sample_id": "sample_001", + "count": 42, + "ratio": 3.14, + } + + csv_output = convert_form_data_to_csv(form_data) + + assert "42" in csv_output + assert "3.14" in csv_output + + +def test_convert_with_list(): + """Test converting data with list values.""" + form_data = { + "sample": "test", + "files": ["file1.txt", "file2.txt"], + } + + csv_output = convert_form_data_to_csv(form_data) + + assert "file1.txt;file2.txt" in csv_output + + +def test_convert_with_dict(): + """Test converting data with dict values.""" + form_data = { + "sample": "test", + "metadata": {"type": "experiment", "id": 1}, + } + + csv_output = convert_form_data_to_csv(form_data) + + assert "metadata" in csv_output + assert "type" in csv_output or "experiment" in csv_output + + +def test_convert_empty_data_raises_error(): + """Test that empty form data raises ValueError.""" + with pytest.raises(ValueError, match="formData cannot be empty"): + convert_form_data_to_csv({}) + + +def test_convert_with_none_values(): + """Test converting data with None values.""" + form_data = { + "sample": "test", + "optional_field": None, + } + + csv_output = convert_form_data_to_csv(form_data) + + lines = csv_output.strip().split("\n") + assert len(lines) == 2 + + +@pytest.mark.asyncio +@respx.mock +async def test_create_dataset_success(): + """Test successful dataset creation.""" + route = respx.post(url__regex=r".*/workspaces/.*/datasets/").mock( + return_value=httpx.Response( + 200, + json={ + "dataset": { + "id": "dataset_123", + "name": "test-dataset", + } + }, + ) + ) + + result = await create_seqera_dataset(name="test-dataset", description="Test description") + + assert isinstance(result, DatasetCreationResult) + assert result.dataset_id == "dataset_123" + assert result.raw_response["dataset"]["name"] == "test-dataset" + assert route.called + + +@pytest.mark.asyncio +@respx.mock +async def test_create_dataset_default_name(): + """Test dataset creation with auto-generated name.""" + route = respx.post(url__regex=r".*/workspaces/.*/datasets/").mock( + return_value=httpx.Response( + 200, + json={ + "dataset": { + "id": "dataset_456", + "name": "dataset-1234567890", + } + }, + ) + ) + + result = await create_seqera_dataset() + + assert result.dataset_id == "dataset_456" + # Verify a name was generated + assert route.called + + +@pytest.mark.asyncio +@respx.mock +async def test_create_dataset_api_error(): + """Test dataset creation with API error.""" + respx.post(url__regex=r".*/workspaces/.*/datasets/").mock( + return_value=httpx.Response(400, text="Bad request") + ) + + with pytest.raises(SeqeraServiceError, match="400"): + await create_seqera_dataset(name="test") + + +@pytest.mark.asyncio +@respx.mock +async def test_create_dataset_missing_id_in_response(): + """Test handling when response is missing dataset ID.""" + respx.post(url__regex=r".*/workspaces/.*/datasets/").mock( + return_value=httpx.Response( + 200, + json={ + "dataset": { + "name": "test-dataset", + # Missing "id" field + } + }, + ) + ) + + with pytest.raises(SeqeraServiceError, match="response lacked dataset id"): + await create_seqera_dataset(name="test") + + +@pytest.mark.asyncio +@respx.mock +async def test_upload_success(): + """Test successful dataset upload.""" + route = respx.post(url__regex=r".*/workspaces/.*/datasets/.*/upload").mock( + return_value=httpx.Response( + 200, + json={ + "version": {"datasetId": "dataset_789"}, + "message": "Upload successful", + }, + ) + ) + + form_data = { + "sample": "test_sample", + "input": "/path/file.txt", + } + + result = await upload_dataset_to_seqera(dataset_id="dataset_789", form_data=form_data) + + assert isinstance(result, DatasetUploadResult) + assert result.success is True + assert result.dataset_id == "dataset_789" + assert route.called + + +@pytest.mark.asyncio +@respx.mock +async def test_upload_creates_csv(): + """Test that upload creates proper CSV.""" + route = respx.post(url__regex=r".*/workspaces/.*/datasets/.*/upload").mock( + return_value=httpx.Response(200, json={}) + ) + + form_data = { + "col1": "value1", + "col2": "value2", + } + + await upload_dataset_to_seqera("dataset_123", form_data) + + # Verify POST was called + assert route.called + # The CSV data should be in the request + request = route.calls.last.request + assert request.headers.get("content-type", "").startswith("multipart/form-data") + + +@pytest.mark.asyncio +@respx.mock +async def test_upload_api_error(): + """Test upload with API error.""" + respx.post(url__regex=r".*/workspaces/.*/datasets/.*/upload").mock( + return_value=httpx.Response(500, text="Server error") + ) + + form_data = {"sample": "test"} + + with pytest.raises(SeqeraServiceError, match="500"): await upload_dataset_to_seqera("dataset_123", form_data) - # Verify POST was called with files - call_args = mock_client.post.call_args - assert "files" in call_args[1] - - @patch("app.services.datasets.httpx.AsyncClient") - async def test_upload_api_error(self, mock_client_class): - """Test upload with API error.""" - mock_response = AsyncMock() - mock_response.is_error = True - mock_response.status_code = 500 - mock_response.text = "Server error" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form_data = {"sample": "test"} - - with pytest.raises(SeqeraServiceError, match="500"): - await upload_dataset_to_seqera("dataset_123", form_data) - - @patch("app.services.datasets.httpx.AsyncClient") - async def test_upload_with_complex_data(self, mock_client_class): - """Test upload with complex form data.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.status_code = 200 - mock_response.text = "" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form_data = { - "sample": "test", - "files": ["file1.txt", "file2.txt"], - "count": 42, - "metadata": {"type": "test"}, - } - - result = await upload_dataset_to_seqera("dataset_123", form_data) - - assert result.success is True + +@pytest.mark.asyncio +@respx.mock +async def test_upload_with_complex_data(): + """Test upload with complex form data.""" + respx.post(url__regex=r".*/workspaces/.*/datasets/.*/upload").mock( + return_value=httpx.Response(200, json={}) + ) + + form_data = { + "sample": "test", + "files": ["file1.txt", "file2.txt"], + "count": 42, + "metadata": {"type": "test"}, + } + + result = await upload_dataset_to_seqera("dataset_123", form_data) + + assert result.success is True diff --git a/tests/test_services_seqera.py b/tests/test_services_seqera.py index 4e99ba0..f60bd2f 100644 --- a/tests/test_services_seqera.py +++ b/tests/test_services_seqera.py @@ -3,9 +3,10 @@ from __future__ import annotations import os -from unittest.mock import AsyncMock, MagicMock, patch +import httpx import pytest +import respx from app.schemas.workflows import WorkflowLaunchForm from app.services.seqera import ( @@ -17,221 +18,192 @@ ) -class TestGetRequiredEnv: - """Tests for _get_required_env helper.""" +def test_get_existing_env_variable(): + """Test getting an existing environment variable.""" + result = _get_required_env("SEQERA_API_URL") + assert result == "https://api.seqera.test" - def test_get_existing_env_variable(self): - """Test getting an existing environment variable.""" - result = _get_required_env("SEQERA_API_URL") - assert result == "https://api.seqera.test" - def test_get_missing_env_variable(self): - """Test that missing env variable raises error.""" - with pytest.raises(SeqeraConfigurationError, match="MISSING_VAR"): - _get_required_env("MISSING_VAR") +def test_get_missing_env_variable(): + """Test that missing env variable raises error.""" + with pytest.raises(SeqeraConfigurationError, match="MISSING_VAR"): + _get_required_env("MISSING_VAR") -class TestLaunchSeqeraWorkflow: - """Tests for launch_seqera_workflow function.""" +@pytest.mark.asyncio +@respx.mock +async def test_launch_success_minimal(): + """Test successful workflow launch with minimal parameters.""" + # Mock the Seqera API + route = respx.post("https://api.seqera.test/workflow/launch").mock( + return_value=httpx.Response( + 200, + json={"workflowId": "wf_test_123"}, + ) + ) + + # Create form + form = WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + ) + + # Execute + result = await launch_seqera_workflow(form) + + # Verify result + assert isinstance(result, SeqeraLaunchResult) + assert result.workflow_id == "wf_test_123" + assert result.status == "submitted" + + # Verify API was called once + assert route.called + assert route.call_count == 1 + + +@pytest.mark.asyncio +@respx.mock +async def test_launch_success_with_all_params(): + """Test successful launch with all parameters.""" + route = respx.post(url__regex=r".*/workflow/launch.*").mock( + return_value=httpx.Response( + 200, + json={"workflowId": "wf_full_456"}, + ) + ) - @patch("app.services.seqera.httpx.AsyncClient") - async def test_launch_success_minimal(self, mock_client_class): - """Test successful workflow launch with minimal parameters.""" - # Setup mock - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = { - "workflowId": "wf_test_123", - } - mock_response.reason_phrase = "OK" + form = WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + revision="main", + runName="my-custom-run", + configProfiles=["docker", "test"], + paramsText="custom_param: value", + ) - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client + result = await launch_seqera_workflow(form, dataset_id="dataset_789") - # Create form - form = WorkflowLaunchForm( - pipeline="https://github.com/test/repo", - ) + assert result.workflow_id == "wf_full_456" - # Execute - result = await launch_seqera_workflow(form) - - # Verify - assert isinstance(result, SeqeraLaunchResult) - assert result.workflow_id == "wf_test_123" - assert result.status == "submitted" - - # Verify API call - mock_client.post.assert_called_once() - call_args = mock_client.post.call_args - assert "https://api.seqera.test/workflow/launch" in call_args[0][0] - - @patch("app.services.seqera.httpx.AsyncClient") - async def test_launch_success_with_all_params(self, mock_client_class): - """Test successful launch with all parameters.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = { - "workflowId": "wf_full_456", - } - mock_response.reason_phrase = "OK" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form = WorkflowLaunchForm( - pipeline="https://github.com/test/repo", - revision="main", - runName="my-custom-run", - configProfiles=["docker", "test"], - paramsText="custom_param: value", - ) + # Verify the payload includes dataset + assert route.called + request = route.calls.last.request + # Read the request body and parse JSON + import json + payload = json.loads(request.content) + assert "datasetIds" in payload["launch"] + assert "dataset_789" in payload["launch"]["datasetIds"] + + +@pytest.mark.asyncio +@respx.mock +async def test_launch_includes_default_params(): + """Test that default parameters are included.""" + route = respx.post(url__regex=r".*/workflow/launch.*").mock( + return_value=httpx.Response(200, json={"workflowId": "wf_123"}) + ) + + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + await launch_seqera_workflow(form) - result = await launch_seqera_workflow(form, dataset_id="dataset_789") + # Check request payload + request = route.calls.last.request + import json + payload = json.loads(request.content) + params_text = payload["launch"]["paramsText"] - assert result.workflow_id == "wf_full_456" + # Check default params are included + assert "use_dgxa100: false" in params_text + assert 'project: "za08"' in params_text + assert "outdir:" in params_text - # Verify the payload includes dataset - call_args = mock_client.post.call_args - payload = call_args[1]["json"] - assert "datasetIds" in payload["launch"] - assert "dataset_789" in payload["launch"]["datasetIds"] - @patch("app.services.seqera.httpx.AsyncClient") - async def test_launch_includes_default_params(self, mock_client_class): - """Test that default parameters are included.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = {"workflowId": "wf_123"} - mock_response.reason_phrase = "OK" +@pytest.mark.asyncio +@respx.mock +async def test_launch_with_dataset_adds_input_url(): + """Test that providing a dataset ID adds it to launch payload.""" + route = respx.post(url__regex=r".*/workflow/launch.*").mock( + return_value=httpx.Response(200, json={"workflowId": "wf_dataset_999"}) + ) - mock_client = MagicMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + await launch_seqera_workflow(form, dataset_id="ds_abc") + # Verify request payload + request = route.calls.last.request + import json + payload = json.loads(request.content) + params_text = payload["launch"]["paramsText"] + + assert "input:" in params_text + assert "ds_abc" in params_text + assert "samplesheet.csv" in params_text + + +@pytest.mark.asyncio +@respx.mock +async def test_launch_api_error_response(): + """Test handling of API error response.""" + respx.post(url__regex=r".*/workflow/launch.*").mock( + return_value=httpx.Response(400, text="Invalid request") + ) + + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + with pytest.raises(SeqeraServiceError, match="400"): await launch_seqera_workflow(form) - call_args = mock_client.post.call_args - payload = call_args[1]["json"] - params_text = payload["launch"]["paramsText"] - - # Check default params are included - assert "use_dgxa100: false" in params_text - assert 'project: "za08"' in params_text - assert "outdir:" in params_text - - @patch("app.services.seqera.httpx.AsyncClient") - async def test_launch_with_dataset_adds_input_url(self, mock_client_class): - """Test that providing a dataset ID adds it to launch payload.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = {"workflowId": "wf_dataset_999"} - mock_response.reason_phrase = "OK" - - mock_client = MagicMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - - await launch_seqera_workflow(form, dataset_id="ds_abc") - - call_args = mock_client.post.call_args - payload = call_args[1]["json"] - params_text = payload["launch"]["paramsText"] - - assert "input:" in params_text - assert "ds_abc" in params_text - assert "samplesheet.csv" in params_text - - @patch("app.services.seqera.httpx.AsyncClient") - async def test_launch_api_error_response(self, mock_client_class): - """Test handling of API error response.""" - mock_response = AsyncMock() - mock_response.is_error = True - mock_response.status_code = 400 - mock_response.text = "Invalid request" - mock_response.reason_phrase = "Bad Request" - - mock_client = AsyncMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - - with pytest.raises(SeqeraServiceError, match="400"): - await launch_seqera_workflow(form) - - @patch("app.services.seqera.httpx.AsyncClient") - async def test_launch_missing_workflow_id_in_response(self, mock_client_class): - """Test error handling when API response lacks workflowId.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = {"status": "success"} - mock_response.reason_phrase = "OK" - - mock_client = MagicMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - - with pytest.raises(SeqeraServiceError, match="workflowId"): - await launch_seqera_workflow(form) - - def test_launch_missing_env_vars(self): - """Test that missing environment variables raise error.""" - form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") - - with patch.dict(os.environ, {}, clear=True): - with pytest.raises(SeqeraConfigurationError): - # This will fail synchronously when trying to get env vars - import asyncio - - asyncio.run(launch_seqera_workflow(form)) - - @patch("app.services.seqera.httpx.AsyncClient") - async def test_launch_with_custom_params_text(self, mock_client_class): - """Test launch with custom paramsText.""" - mock_response = MagicMock() - mock_response.is_error = False - mock_response.json.return_value = {"workflowId": "wf_params_xyz"} - mock_response.reason_phrase = "OK" - - mock_client = MagicMock() - mock_client.post = AsyncMock(return_value=mock_response) - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client_class.return_value = mock_client - - form = WorkflowLaunchForm( - pipeline="https://github.com/test/repo", - paramsText="my_custom_param: 42\nanother_param: test", - ) +@pytest.mark.asyncio +@respx.mock +async def test_launch_missing_workflow_id_in_response(): + """Test error handling when API response lacks workflowId.""" + respx.post(url__regex=r".*/workflow/launch.*").mock( + return_value=httpx.Response(200, json={"status": "success"}) + ) + + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + with pytest.raises(SeqeraServiceError, match="workflowId"): await launch_seqera_workflow(form) - call_args = mock_client.post.call_args - payload = call_args[1]["json"] - params_text = payload["launch"]["paramsText"] - # Should contain both default and custom params - assert "use_dgxa100: false" in params_text # default - assert "my_custom_param: 42" in params_text # custom - assert "another_param: test" in params_text # custom +def test_launch_missing_env_vars(): + """Test that missing environment variables raise error.""" + form = WorkflowLaunchForm(pipeline="https://github.com/test/repo") + + with pytest.MonkeyPatch.context() as mp: + mp.delenv("SEQERA_API_URL", raising=False) + mp.delenv("SEQERA_ACCESS_TOKEN", raising=False) + mp.delenv("WORK_SPACE", raising=False) + + with pytest.raises(SeqeraConfigurationError): + import asyncio + asyncio.run(launch_seqera_workflow(form)) + + +@pytest.mark.asyncio +@respx.mock +async def test_launch_with_custom_params_text(): + """Test launch with custom paramsText.""" + route = respx.post(url__regex=r".*/workflow/launch.*").mock( + return_value=httpx.Response(200, json={"workflowId": "wf_params_xyz"}) + ) + + form = WorkflowLaunchForm( + pipeline="https://github.com/test/repo", + paramsText="my_custom_param: 42\nanother_param: test", + ) + + await launch_seqera_workflow(form) + + # Verify request payload + request = route.calls.last.request + import json + payload = json.loads(request.content) + params_text = payload["launch"]["paramsText"] + + # Should contain both default and custom params + assert "use_dgxa100: false" in params_text # default + assert "my_custom_param: 42" in params_text # custom + assert "another_param: test" in params_text # custom diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..494295c --- /dev/null +++ b/uv.lock @@ -0,0 +1,1200 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "black" +version = "23.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/f4/a57cde4b60da0e249073009f4a9087e9e0a955deae78d3c2a493208d0c5c/black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5", size = 620809, upload-time = "2023-12-22T23:06:17.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/58/677da52d845b59505a8a787ff22eff9cfd9046b5789aa2bd387b236db5c5/black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2", size = 1560531, upload-time = "2023-12-22T23:18:20.555Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/522a4f1e4b2b8da62e4ec0cb8acf2d257e6d39b31f4214f0fd94d2eeb5bd/black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba", size = 1404644, upload-time = "2023-12-22T23:17:46.425Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dc/af67d8281e9a24f73d24b060f3f03f6d9ad6be259b3c6acef2845e17d09c/black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0", size = 1711153, upload-time = "2023-12-22T23:08:34.4Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0f/94d7c36b421ea187359c413be7b9fc66dc105620c3a30b1c94310265830a/black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3", size = 1332918, upload-time = "2023-12-22T23:10:28.188Z" }, + { url = "https://files.pythonhosted.org/packages/ed/2c/d9b1a77101e6e5f294f6553d76c39322122bfea2a438aeea4eb6d4b22749/black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba", size = 1541926, upload-time = "2023-12-22T23:23:17.72Z" }, + { url = "https://files.pythonhosted.org/packages/72/e2/d981a3ff05ba9abe3cfa33e70c986facb0614fd57c4f802ef435f4dd1697/black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b", size = 1388465, upload-time = "2023-12-22T23:19:00.611Z" }, + { url = "https://files.pythonhosted.org/packages/eb/59/1f5c8eb7bba8a8b1bb5c87f097d16410c93a48a6655be3773db5d2783deb/black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59", size = 1691993, upload-time = "2023-12-22T23:08:32.018Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/a80abc6fcdb00f0d4d3d74184b172adbf2197f6b002913fa0fb6af4dc6db/black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50", size = 1340929, upload-time = "2023-12-22T23:09:37.088Z" }, + { url = "https://files.pythonhosted.org/packages/66/16/8726cedc83be841dfa854bbeef1288ee82272282a71048d7935292182b0b/black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e", size = 1569989, upload-time = "2023-12-22T23:20:22.158Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1e/30f5eafcc41b8378890ba39b693fa111f7dca8a2620ba5162075d95ffe46/black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec", size = 1398647, upload-time = "2023-12-22T23:19:57.225Z" }, + { url = "https://files.pythonhosted.org/packages/99/de/ddb45cc044256431d96d846ce03164d149d81ca606b5172224d1872e0b58/black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e", size = 1720450, upload-time = "2023-12-22T23:08:52.675Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/54e5dbe9be5a10cbea2259517206ff7b6a452bb34e07508c7e1395950833/black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9", size = 1351070, upload-time = "2023-12-22T23:09:32.762Z" }, + { url = "https://files.pythonhosted.org/packages/7b/14/4da7b12a9abc43a601c215cb5a3d176734578da109f0dbf0a832ed78be09/black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e", size = 194363, upload-time = "2023-12-22T23:06:14.278Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "faker" +version = "40.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/1d/aa43ef59589ddf3647df918143f1bac9eb004cce1c43124ee3347061797d/faker-40.1.0.tar.gz", hash = "sha256:c402212a981a8a28615fea9120d789e3f6062c0c259a82bfb8dff5d273e539d2", size = 1948784, upload-time = "2025-12-29T18:06:00.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/23/e22da510e1ec1488966330bf76d8ff4bd535cbfc93660eeb7657761a1bb2/faker-40.1.0-py3-none-any.whl", hash = "sha256:a616d35818e2a2387c297de80e2288083bc915e24b7e39d2fb5bc66cce3a929f", size = 1985317, upload-time = "2025-12-29T18:05:58.831Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/84/2cfb1f3b9b60bab52e16a220c931223fc8e963d0d7bb9132bef012aafc3f/librt-0.7.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0", size = 54709, upload-time = "2026-01-01T23:50:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/19/a1/3127b277e9d3784a8040a54e8396d9ae5c64d6684dc6db4b4089b0eedcfb/librt-0.7.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8", size = 56658, upload-time = "2026-01-01T23:50:49.74Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e9/b91b093a5c42eb218120445f3fef82e0b977fa2225f4d6fc133d25cdf86a/librt-0.7.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06", size = 161026, upload-time = "2026-01-01T23:50:50.853Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cb/1ded77d5976a79d7057af4a010d577ce4f473ff280984e68f4974a3281e5/librt-0.7.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495", size = 169529, upload-time = "2026-01-01T23:50:52.24Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/6ca5bdaa701e15f05000ac1a4c5d1475c422d3484bd3d1ca9e8c2f5be167/librt-0.7.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0", size = 183271, upload-time = "2026-01-01T23:50:55.287Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2d/55c0e38073997b4bbb5ddff25b6d1bbba8c2f76f50afe5bb9c844b702f34/librt-0.7.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e", size = 179039, upload-time = "2026-01-01T23:50:56.807Z" }, + { url = "https://files.pythonhosted.org/packages/33/4e/3662a41ae8bb81b226f3968426293517b271d34d4e9fd4b59fc511f1ae40/librt-0.7.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c", size = 173505, upload-time = "2026-01-01T23:50:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5d/cf768deb8bdcbac5f8c21fcb32dd483d038d88c529fd351bbe50590b945d/librt-0.7.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8", size = 193570, upload-time = "2026-01-01T23:50:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/ee70effd13f1d651976d83a2812391f6203971740705e3c0900db75d4bce/librt-0.7.7-cp310-cp310-win32.whl", hash = "sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900", size = 42600, upload-time = "2026-01-01T23:51:00.694Z" }, + { url = "https://files.pythonhosted.org/packages/f0/eb/dc098730f281cba76c279b71783f5de2edcba3b880c1ab84a093ef826062/librt-0.7.7-cp310-cp310-win_amd64.whl", hash = "sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58", size = 48977, upload-time = "2026-01-01T23:51:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/f0/56/30b5c342518005546df78841cb0820ae85a17e7d07d521c10ef367306d0d/librt-0.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee", size = 54709, upload-time = "2026-01-01T23:51:02.774Z" }, + { url = "https://files.pythonhosted.org/packages/72/78/9f120e3920b22504d4f3835e28b55acc2cc47c9586d2e1b6ba04c3c1bf01/librt-0.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8", size = 56663, upload-time = "2026-01-01T23:51:03.838Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/7d7a1ee7dfc1151836028eba25629afcf45b56bbc721293e41aa2e9b8934/librt-0.7.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85", size = 161705, upload-time = "2026-01-01T23:51:04.917Z" }, + { url = "https://files.pythonhosted.org/packages/45/a5/952bc840ac8917fbcefd6bc5f51ad02b89721729814f3e2bfcc1337a76d6/librt-0.7.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99", size = 171029, upload-time = "2026-01-01T23:51:06.09Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bf/c017ff7da82dc9192cf40d5e802a48a25d00e7639b6465cfdcee5893a22c/librt-0.7.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb", size = 184704, upload-time = "2026-01-01T23:51:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/77/ec/72f3dd39d2cdfd6402ab10836dc9cbf854d145226062a185b419c4f1624a/librt-0.7.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74", size = 180719, upload-time = "2026-01-01T23:51:09.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/86/06e7a1a81b246f3313bf515dd9613a1c81583e6fd7843a9f4d625c4e926d/librt-0.7.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75", size = 174537, upload-time = "2026-01-01T23:51:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/83/08/f9fb2edc9c7a76e95b2924ce81d545673f5b034e8c5dd92159d1c7dae0c6/librt-0.7.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28", size = 195238, upload-time = "2026-01-01T23:51:11.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/56/ea2d2489d3ea1f47b301120e03a099e22de7b32c93df9a211e6ff4f9bf38/librt-0.7.7-cp311-cp311-win32.whl", hash = "sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151", size = 42939, upload-time = "2026-01-01T23:51:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/c288f417e42ba2a037f1c0753219e277b33090ed4f72f292fb6fe175db4c/librt-0.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a", size = 49240, upload-time = "2026-01-01T23:51:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/738eb33a6c1516fdb2dfd2a35db6e5300f7616679b573585be0409bc6890/librt-0.7.7-cp311-cp311-win_arm64.whl", hash = "sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890", size = 42613, upload-time = "2026-01-01T23:51:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, + { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, + { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, + { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, + { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, + { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, + { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/28/2e/83722ece0f6ee24387d6cb830dd562ddbcd6ce0b9d76072c6849670c31b4/pathspec-1.0.1.tar.gz", hash = "sha256:e2769b508d0dd47b09af6ee2c75b2744a2cb1f474ae4b1494fd6a1b7a841613c", size = 129791, upload-time = "2026-01-06T13:02:55.15Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fe/2257c71721aeab6a6e8aa1f00d01f2a20f58547d249a6c8fef5791f559fc/pathspec-1.0.1-py3-none-any.whl", hash = "sha256:8870061f22c58e6d83463cfce9a7dd6eca0512c772c1001fb09ac64091816721", size = 54584, upload-time = "2026-01-06T13:02:53.601Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "polyfactory" +version = "2.22.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/c6/517137955bb3764e813edfa0086e8b3675dc126c49428e8482a70018ec33/polyfactory-2.22.5.tar.gz", hash = "sha256:d641d8c10c7d3e1f0f862ba2733ad1523069af58614bb1c077aeccabb66d26b7", size = 264946, upload-time = "2025-11-15T09:52:39.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/87/6cd69c43e86bc5863f93dba3c9fb42ee4fda36a0fe5aa3cbd25001fb26f8/polyfactory-2.22.5-py3-none-any.whl", hash = "sha256:822d1af463520153200b4b62b06b0dc73a1d5edc8911e2e63ed0757ae21cc2b3", size = 63934, upload-time = "2025-11-15T09:52:37.956Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "sbp-backend" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "coverage", extra = ["toml"] }, + { name = "httpx" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "polyfactory" }, + { name = "respx" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = "~=23.3" }, + { name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = "~=7.4" }, + { name = "fastapi", specifier = "~=0.111" }, + { name = "httpx", specifier = "~=0.27" }, + { name = "httpx", marker = "extra == 'dev'", specifier = "~=0.28" }, + { name = "mypy", marker = "extra == 'dev'", specifier = "~=1.19" }, + { name = "pydantic", specifier = "~=2.7" }, + { name = "pytest", marker = "extra == 'dev'", specifier = "~=8.3" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "~=0.23" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = "~=4.1" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = "~=3.12" }, + { name = "python-dotenv", specifier = "~=1.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "~=0.14" }, + { name = "uvicorn", extras = ["standard"], specifier = "~=0.29" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "polyfactory", specifier = "~=2.16" }, + { name = "respx", specifier = "~=0.21" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] From 9f5beffde0788a7777e501c1bc7e3af0834c5c5a Mon Sep 17 00:00:00 2001 From: laclac102 Date: Wed, 7 Jan 2026 09:35:15 +1100 Subject: [PATCH 13/15] fix: update ci --- .github/workflows/lint.yml | 17 +++++++----- .github/workflows/test-coverage.yml | 15 +++++----- Dockerfile | 16 +++++++---- README.md | 43 +++++++++++++---------------- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index db2fc08..95a93be 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,18 +17,21 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" - cache: "pip" - cache-dependency-path: requirements.txt + + - name: Install UV + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt + uv sync --all-extras + - name: Run Ruff - run: ruff check app tests + run: uv run ruff check app tests - name: Run Black - run: black --check app tests + run: uv run black --check app tests - name: Run MyPy - run: mypy app --ignore-missing-imports + run: uv run mypy app --ignore-missing-imports diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 17d6261..e60063f 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -21,14 +21,15 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "pip" - cache-dependency-path: requirements.txt + + - name: Install UV + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - python -m pip install -r requirements-dev.txt + uv sync --all-extras - name: Run tests with coverage env: @@ -39,11 +40,11 @@ jobs: COMPUTE_ID: compute-123 WORK_DIR: /tmp/work run: | - pytest --cov=app --cov-report=xml --cov-report=term-missing --cov-report=html -v + uv run pytest --cov=app --cov-report=xml --cov-report=term-missing --cov-report=html -v - name: Check coverage threshold (90%) run: | - coverage report --fail-under=90 + uv run coverage report --fail-under=90 - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/Dockerfile b/Dockerfile index 22a7ee9..30b6b74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,17 +2,21 @@ FROM python:3.11-slim AS base ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 + PYTHONUNBUFFERED=1 WORKDIR /app -COPY requirements.txt ./ -RUN pip install --upgrade pip && \ - pip install --no-cache-dir -r requirements.txt +# Install UV +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# Copy dependency files +COPY pyproject.toml uv.lock ./ + +# Install dependencies +RUN uv sync --frozen --no-dev COPY app ./app EXPOSE 3000 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "3000"] +CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "3000"] diff --git a/README.md b/README.md index 1aac502..956dad2 100644 --- a/README.md +++ b/README.md @@ -8,31 +8,28 @@ FastAPI backend for handling Seqera Platform workflow launches. ## Prerequisites -- Python 3.10+ (matching the version used by your deployment target) -- [uvicorn](https://www.uvicorn.org/) and other dependencies listed in `requirements.txt` +- Python 3.10+ +- [UV](https://docs.astral.sh/uv/) package manager ## Setup -1. Create a virtual environment (recommended): +1. Install UV (if not already installed): ```bash - python3 -m venv .venv - source .venv/bin/activate + # macOS/Linux + curl -LsSf https://astral.sh/uv/install.sh | sh + + # Windows + powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` 2. Install dependencies: ```bash - pip install -r requirements.txt + uv sync --all-extras ``` -3. Install development dependencies (for testing and linting): - - ```bash - pip install -r requirements-dev.txt - ``` - -4. Configure environment variables: +3. Configure environment variables: ```bash cp .env.example .env @@ -42,8 +39,7 @@ FastAPI backend for handling Seqera Platform workflow launches. 4. Run the API locally: ```bash - uvicorn app.main:app --reload --host 0.0.0.0 --port 3000 - # or: python -m app.main (uses PORT/UVICORN_RELOAD variables) + uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 3000 ``` ## API Endpoints @@ -62,16 +58,16 @@ Run the test suite with coverage: ```bash # Run all tests with coverage report -pytest --cov=app --cov-report=term-missing --cov-report=html +uv run pytest --cov=app --cov-report=term-missing --cov-report=html # Run tests with verbose output -pytest -v +uv run pytest -v # Run specific test file -pytest tests/test_main.py +uv run pytest tests/test_main.py # Check coverage threshold (90%) -coverage report --fail-under=90 +uv run coverage report --fail-under=90 ``` View HTML coverage report: @@ -86,17 +82,16 @@ start htmlcov/index.html # Windows (Command Prompt / PowerShell) ```bash # Run ruff linter -ruff check app tests +uv run ruff check app tests # Run black formatter -black app tests +uv run black app tests # Run type checking with mypy -mypy app --ignore-missing-imports +uv run mypy app --ignore-missing-imports # Install pre-commit hooks -pip install pre-commit -pre-commit install +uv run pre-commit install # Run pre-commit on all files pre-commit run --all-files From a3e068b2a679023242cf06e44173c8de4134d4db Mon Sep 17 00:00:00 2001 From: laclac102 Date: Wed, 7 Jan 2026 09:43:37 +1100 Subject: [PATCH 14/15] fix: improve lint --- tests/conftest.py | 15 ++------------- tests/test_services_seqera.py | 9 ++++++--- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 657f1f5..05cec8d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,6 @@ from collections.abc import AsyncGenerator, Generator import pytest -import respx from fastapi.testclient import TestClient from httpx import AsyncClient from polyfactory.factories.pydantic_factory import ModelFactory @@ -30,10 +29,8 @@ WorkflowLaunchResponse, ) - -# ============================================================================ -# Polyfactory Factories - Auto-generate test data from Pydantic schemas # ============================================================================ +# Auto-generate test data from Pydantic schemas # ============================================================================ class WorkflowLaunchFormFactory(ModelFactory[WorkflowLaunchForm]): @@ -110,18 +107,10 @@ async def async_client(app) -> AsyncGenerator[AsyncClient, None]: yield ac -# ============================================================================ -# Legacy Fixtures (kept for backward compatibility) -# Note: Consider using factories directly in tests instead -# ============================================================================ -# Note: respx is now used for HTTP mocking instead of manual AsyncMock -# respx automatically handles httpx.AsyncClient mocking - - @pytest.fixture def sample_workflow_launch_form(): """Sample workflow launch form data. - + NOTE: Consider using WorkflowLaunchFormFactory.build() directly in tests. """ return { diff --git a/tests/test_services_seqera.py b/tests/test_services_seqera.py index f60bd2f..185f79b 100644 --- a/tests/test_services_seqera.py +++ b/tests/test_services_seqera.py @@ -2,8 +2,6 @@ from __future__ import annotations -import os - import httpx import pytest import respx @@ -88,6 +86,7 @@ async def test_launch_success_with_all_params(): request = route.calls.last.request # Read the request body and parse JSON import json + payload = json.loads(request.content) assert "datasetIds" in payload["launch"] assert "dataset_789" in payload["launch"]["datasetIds"] @@ -108,6 +107,7 @@ async def test_launch_includes_default_params(): # Check request payload request = route.calls.last.request import json + payload = json.loads(request.content) params_text = payload["launch"]["paramsText"] @@ -132,6 +132,7 @@ async def test_launch_with_dataset_adds_input_url(): # Verify request payload request = route.calls.last.request import json + payload = json.loads(request.content) params_text = payload["launch"]["paramsText"] @@ -176,9 +177,10 @@ def test_launch_missing_env_vars(): mp.delenv("SEQERA_API_URL", raising=False) mp.delenv("SEQERA_ACCESS_TOKEN", raising=False) mp.delenv("WORK_SPACE", raising=False) - + with pytest.raises(SeqeraConfigurationError): import asyncio + asyncio.run(launch_seqera_workflow(form)) @@ -200,6 +202,7 @@ async def test_launch_with_custom_params_text(): # Verify request payload request = route.calls.last.request import json + payload = json.loads(request.content) params_text = payload["launch"]["paramsText"] From dd387a3d90fcb2757875b3d5c42b2c75d8be84eb Mon Sep 17 00:00:00 2001 From: laclac102 Date: Wed, 7 Jan 2026 14:35:35 +1100 Subject: [PATCH 15/15] fix: update ci --- .github/workflows/test-coverage.yml | 10 ---------- .pre-commit-config.yaml | 2 +- pyproject.toml | 8 ++------ tests/test_main.py | 23 +++++++++++++++++++++-- uv.lock | 16 ++++------------ 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index e60063f..50dcd08 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -46,18 +46,8 @@ jobs: run: | uv run coverage report --fail-under=90 - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - if: matrix.python-version == '3.11' - with: - file: ./coverage.xml - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false - - name: Coverage summary uses: irongut/CodeCoverageSummary@v1.3.0 - if: matrix.python-version == '3.11' with: filename: coverage.xml badge: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 883229b..6ca0537 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.15 + rev: v0.14.10 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/pyproject.toml b/pyproject.toml index 7641a40..4859fbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ dev = [ "ruff~=0.14", "black~=23.3", "mypy~=1.19", + "polyfactory~=2.16", + "respx~=0.21", ] [tool.pytest.ini_options] @@ -113,9 +115,3 @@ warn_return_any = true warn_unused_configs = true disallow_untyped_defs = false ignore_missing_imports = true - -[dependency-groups] -dev = [ - "polyfactory~=2.16", - "respx~=0.21", -] diff --git a/tests/test_main.py b/tests/test_main.py index 0da91ec..0d6ae56 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -77,7 +77,16 @@ def test_cors_allowed_origins_parsing(): os.environ, {"ALLOWED_ORIGINS": "http://localhost:3000, http://localhost:4200"} ): app = create_app() - assert app is not None + + cors_options = next(mw.kwargs for mw in app.user_middleware if "CORSMiddleware" in str(mw)) + + # Verify the parsed origins are correctly set in the middleware + allowed_origins = cors_options["allow_origins"] + + # Check that both origins are present after parsing + assert "http://localhost:3000" in allowed_origins + assert "http://localhost:4200" in allowed_origins + assert len(allowed_origins) == 2 def test_cors_allowed_origins_with_empty_values(): @@ -88,4 +97,14 @@ def test_cors_allowed_origins_with_empty_values(): os.environ, {"ALLOWED_ORIGINS": "http://localhost:3000,, , http://localhost:4200"} ): app = create_app() - assert app is not None + + cors_options = next(mw.kwargs for mw in app.user_middleware if "CORSMiddleware" in str(mw)) + + # Verify empty values and whitespace are filtered out + allowed_origins = cors_options["allow_origins"] + + # Should only have 2 valid origins (empty strings filtered out) + assert "http://localhost:3000" in allowed_origins + assert "http://localhost:4200" in allowed_origins + assert len(allowed_origins) == 2 + assert "" not in allowed_origins diff --git a/uv.lock b/uv.lock index 494295c..61cd1b9 100644 --- a/uv.lock +++ b/uv.lock @@ -838,17 +838,13 @@ dev = [ { name = "coverage", extra = ["toml"] }, { name = "httpx" }, { name = "mypy" }, + { name = "polyfactory" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-mock" }, - { name = "ruff" }, -] - -[package.dev-dependencies] -dev = [ - { name = "polyfactory" }, { name = "respx" }, + { name = "ruff" }, ] [package.metadata] @@ -859,23 +855,19 @@ requires-dist = [ { name = "httpx", specifier = "~=0.27" }, { name = "httpx", marker = "extra == 'dev'", specifier = "~=0.28" }, { name = "mypy", marker = "extra == 'dev'", specifier = "~=1.19" }, + { name = "polyfactory", marker = "extra == 'dev'", specifier = "~=2.16" }, { name = "pydantic", specifier = "~=2.7" }, { name = "pytest", marker = "extra == 'dev'", specifier = "~=8.3" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "~=0.23" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = "~=4.1" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = "~=3.12" }, { name = "python-dotenv", specifier = "~=1.0" }, + { name = "respx", marker = "extra == 'dev'", specifier = "~=0.21" }, { name = "ruff", marker = "extra == 'dev'", specifier = "~=0.14" }, { name = "uvicorn", extras = ["standard"], specifier = "~=0.29" }, ] provides-extras = ["dev"] -[package.metadata.requires-dev] -dev = [ - { name = "polyfactory", specifier = "~=2.16" }, - { name = "respx", specifier = "~=0.21" }, -] - [[package]] name = "starlette" version = "0.50.0"