From 53495cad369a306d4e5fade049a40f0017ccd5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=AE=D1=80=D0=B8=D0=B9=20=D0=A7=D0=B5=D1=80=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE?= Date: Fri, 31 Oct 2025 12:27:05 +0300 Subject: [PATCH] initial commit --- __pycache__/get_data.cpython-312.pyc | Bin 0 -> 35012 bytes box-score.json | 1197 ++ game.json | 15204 +++++++++++++++++++++++++ get_data.py | 896 ++ requirements.txt | 3 + 5 files changed, 17300 insertions(+) create mode 100644 __pycache__/get_data.cpython-312.pyc create mode 100644 box-score.json create mode 100644 game.json create mode 100644 get_data.py create mode 100644 requirements.txt diff --git a/__pycache__/get_data.cpython-312.pyc b/__pycache__/get_data.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..90e2755b29e1a083bb8d16328e8853c0697d583d GIT binary patch literal 35012 zcmd75dw5jGl`pz?zuyn3)!h=3XbDNEK@v!S4FZfYLc9eAfo+VfG}?_27LwTA;vu&Z z89QUiCW91bh9<`2_9VF@k3BbX#-7MYCLuF9bA7%$ccz;rQM*m)J65u&G^PLT} zj=RJOoWS>RC;7uXPkvR8>ZJOxn)%f|n!_5vG(Fmr+QV8IraPeDtY>e-VFP;` z4;$Irbl8Nqw#R(ZeAq0~7XeI3l zE7$|Yfx?wu`3X*YMc3|P7o8**P2Ulwbz zP!jYBi||((tPslP$3A8d%3snPt`ru(#2sED_<~hJMX>sqMyLd|Mpy#q(x6(X3i8KP zLiJ0UZ0fbhpAXi)Tk{St<$TyLEDcr!mksJU4r%z1MlI4<9`v7hN-10JE%=pq!m^jR zY^wDOU@y;N*X3b1EP!2~#cs&MUaZSF4U$ge|~YE3^P>ov;;da~CJH{waTW{Q@~}%jUd2FXs)xjlz!SFmFT; zMw017BsVq`pRiNdCG5sb-t!Kq7dl4TQp*_w9hUFc3P|+u7OI z8xEcgpA7T{x`JXG;07TO4u-o=2I1HB_lw=V;Xy-veNQmZH4sdzPlu0EIzsSRV4x>F zsH?9J2LmVDDWVuWHP9^v1p@X2db`r9VDDh*@o>05)VyMa7(CV>4unG8fun)YbHQ*! zpV-wp=zsorUno4ZBGBKxB5<^GMQ7j1{?Lk0Fc9kN4Q*-dsFyzmCt`E6w_213j$e!uM&l1(UCzH~F6K(?RNx@IYu&e$>I`^P(T^J6j*> z>=T2tksIbk?(Yelt3P_Kp5C+Y{l>Ij9yVV%<_?OqLFv}ChFUYNmz(vA0@;(-7GIe2 zD)5CdTf8qU`I)CRy_Ar8KW9+&jW6xu2l2&Uv!w=)z9r(}@DbmwPLCCyH_~#V2T$9KZnSO1L3I z*sXj!QLOlK?+6z$M8LdUQ7iFyF1$$jE*K()*Ezu$;ozDgx)Vm_OYSY01&hMcyw1I$ z%S-2hS}md-1fw`)jA$Z8<}*c1=(iU1TTx!Sm*#R*d=XPFulw{Mf8n?1xO3bRF08Pg zTop+CkY3P)?b-D7N_yS=knSn4qC@(x>M#6X()$d5hC8c%iaW#mt!*N4dV?-yMEXt+ zgu=d~LEj_3m0ePDnN4YJP!#*bL09!t_0=cqs|8>6_U7t6&D94Qs*ip4|NYy){hME% z)`j{8#Li$^6%M5}SQ7$i{^>!(Gd>#j&AvfXollzL%|4*A$v$Z8?UU!d-<~!d3iJ#F zw^BxFU3aJ>6bz>|w5~`CMKFYP)m;QU0a~9nZav!>><@Rts}6-lk$Nbt?(I91HXwM6 zMKp?x&W1WprPcid;k1^mPHAnZCm8Jan?)Dmh&1#>8d;);k}$E49W1^UiHT`J#AB^N}rmLh#7`*2jEnj-c2b)Fci4=h99k z&~Z%cJK2H7Ra}GEP529$FuRAjjE*xn#v0EL-LQG%#VeAwm2tz$n@-o*+41t>oi}aX zY1`t2ZSi$m#SMq+l5yPlnt4Q-WZO`>q=vN6LksOa5{HYs+KWL_zh4{$^oW zOrI!RI$}tf3&#SlY`9@@T--UjbL{MOi!Za7vlmOHpSD%}?v86Ok{ zSIsxJ{!2y;*cTz{kRLB;F>${z8C#aAe^F>{saF4@S_^+CK4l-GPmEgric!mO z_QSns;TRuU2`ZPRf+w={E*l-2xp%Wvb_mS zRTtN(dIof1NQE_o#ZqE3))3Hf)di=98&U&99dhPqh+tJ{2zLIcw#dbwk>S4z*YU>djIHo`YKKLY;WICN)Jx?$(0usr4K)W?O_Y?>GBMMdSRPpGwA=MeU%%?rW`HS0Ra(r{s z+u)~(KJ^WR#X_Vd(kdsG!Aa{RiYMAApe6nzB+|N@))LnpN}K4_HgNJN2s#mdkkS2n z_wJ{MQDqTC7W5NT4@e-Cpwl75?F$Ebh*tr;UGg6d_I4hZ{U-xwcLxK+O{dNMec=B? zJ9_tn5Zmx!UoOmur(eZhth9b3Be^2U1VoG=POM@@Jv9&nxz%nx*$)oM+=J~!NFtvi z?e@V)VFTjVr`0DzU05Mdp)?;(8^CvUuz-%L9Td}if7(PqDHx*ZEmLT5HKpTFRtM~9 zLIWq!BKrYu!(Zs1865`sm1xy3Z52dbi*6Jwx>P@2KcOD0PZrdTYyqLRltd52yqBLz zn3ttCK6=&t;nN>^6C3wm*cH_!EQ@1smQENFmd44Y*DTE`t2eqOX1Kg3VfCl%MOPLj z>{T~Pmrm#sr7I@kJUnGcly1Jd^m^&ev6hr)QFM301jYtHU0_rTTf4V6V=UA z4_~X^GNaX0mHei9ON>t~_Qgsse{HgAGLmR&xwg3Fww5a^PgQTpXu0Bw3CoYDrb> z@LL-vTc?^6t6Q%vX+^>nl_~#vBwV%>2`?+Zjf9t_nl?;5{$71d7d!K(_Kcldx+YV= z`5PxaiQ08hP4tP&)>QqP$(}_07KDx@yvyzrYCa%TtZ(;>|k~jk}_n zcv<~5Z^NxsT-h!@)698FqE+MFDet0a>t*vTt$LXa?XHe)zidrdYVPdf^B0>qtv5Q( zQFQV7(dTK+DN0*hw9XX$>um>G==0kyR3}8U=#qP@p8FfUYiot>Z#Ay1HHQDBFWg$L z{!e8*-0VCH4~V^;S#BKBhy!7(&{0Z*i*P6MzXC>&!tsKZmvY<@%)udkNEOcV>=9LN zMN)WC&_)lqTb?KsQG^4WhC9dmRRfg0iNvo{3R0p zz=vdBOuS6FNZ9YkClh~|IPZJDQkqGX%}+NpG(0op1NM)B^n9hf!BRfRY5oy%rSI{- zMgm_)^6w+UPiB|sx5M8;sqpQGDDN=vM&pyH7Ao-pO7qjZQ$oRg3r1?|>kay9krw6n zGOB&P4!I{@jQzC&8RG}xzk3alM8>X1Ej0lWl264DqBaKKI8);P%zmpwj5l&gN8JEBdyrBBJ zn#K*r+ZQqyz`_zAaxeZUHxBQG#n$p4-)FC=%IA~>y)v%vxpP!U)Oo39_d3)T(xc3z zd!@okw*;Jz#EluKC5H0$N#Mvdv>(jX75hGo(uUx5G=ljUxG0))znAi$zc8Es?j9ww z+_ywlr9^qWnr&7MvOFu#UHDfR$f1J2T zub;+$f_XB6x$yxg1tvTLAozab)dpWYJAVo5Jm%9R&0RJx5&5qv4aB5524X_9iztvZ zd674p{2ve^Q7d_deTF^cze!9=qqHq!9E`E24}zE$Z~%jbYN4U}WJC2+X;rr{K-@z7 z&1^*^dIJjaBb4VwynmdyfPxU6>lT`Q1H^(U^=lufoG&sa{drVpI2);TfcU-m8$|eC zK!Xq!RH#<`Lm-2IO~wC`h@z78?3VKR4Q%44wLN`jz>jF!w{G4e?Ne!sioGlLwzN|Y zinYjzF*klKTVKS31dy75$^Hl{t(V1sAzEobW)wnWCPE{B6NyWF8Gj)jRAHFI7R%}$ z6JFgryfx)08W*CfG2vV05lzZ$AJdO+jJgwM-?X_VVXjG6a zZ`$imc>NPq6Q|#}-4^qk@bvUaz{90Dr#+Vsxz!6@eq&lBAFI=O16)bse$~JxvUh;@G zuk6-$WbvF}3)4(u542Lr?^O;f3G>!g%9}dS%?^-U{;w z2Fa!2!an7T=#A*VOw3+S#PDVC1@132f6QOP+MFx>i4{ujxp%I7tR@gtvzj#KFccrG zH5Mr}8?|xIs!a*2&6kRO4r&zC=u5_dI)b|NN6;~~i34A= zN}n8#pr_5$5Z`N=|N9Uhrh&~K!H{bmh}qN^CKNH-oHAEoV~~qwCN}(yJVrW9k^w2j zta60}cTlNlzN$gGuSmX}a^)}7P_UG?iDty|tI`@$T&H!Cc3NWah%tcJjp>R%BZbsw z#DWZLfkPa$53DTq*x)+BG}GQR$GKw%D+6Y;(-I^8Lolm<-5?U5;n#>K@DiURrw0zU zt&Qv@`+4sTedh@g4uYIrW=}qmVGJ-%6ylYy6S17F$0>P7gy&}n)MdHvPE9@t~ zNDd7zY_?gt+0;ZDWI)S8KO;_nNjxRVDCFZ1or=VfNWw+U!Tq}rq;<#o#FGJJynXM1 z_Ox#I*3H`vZWXHuixvsSq%)>Gtwo8#$J3f~!GI`>A9rtV+m_Y@&vu6yvz^u*Xy4p^ z@IcxaJ}$~)P+dFoGR2N0<0R3dNE02}DyGANT-0w8j}w@>McM*TkBKzNS%al5Qi`FD zo<7J{b0O%<({|-s-?3vo-Mv9^52A%kaIi0sA4v{j=;=Nd4D|#GHVa=H=Md!Urkf^++f)VCo7N(Fdq#8_uTJZ@QMl;RhH=vPjFluTrPG!r3CogqUGE>edg%K8 zLvhQJq~(bjwaR7s?d$eac*KwUna3ime|sSqjuV{BH>seC-#dfD%Q#e zPH71wK6kWgqbakC_q%k%yYC$0XP4Sd4RC1OGVNHBaG*IS&-|i2?pTs^>`YPPlw^!r zRN6w7b{bJ>*X-zJ6#W^*oOUcj&EMBdb;TXal8)At!+HPMizxQf2}i}WqZUnMzt)Do{MmDD`){EOlw|!&BEj3qAKBMCbD)N9pEvUfGS!;sC zSZjZ|?S;1M8rLV*VktS@sBY}ki0+q$;=9sp9c_&^jNGlYxaod4uLwm+f zQUB#m2}?$W*@rI1M$1mz%(f>lKK-gWAKT%$(dAJ_8aU&s6#jQjlRxVCs-l{#Lo!`FC~ zmLPum>gO34zU!LQ6_jtK#@YTJRmHibXYV=7WNs5ozi@BTD7w23k`knYtm|H z)zaEyJ$-?&=%eq(_H+G`u>@B5uM@<|v~)0=V7-vmpX}}(faZ+Wo6Zom6IEVX5zNx^ z$BZ1raeP_>z9V!19+FM#wC)?;MOU`QmcHJW^e!KMR=TdaZ2QTprhy$Iazm%&_v>z5F=u z)_haz<>m4={jVvnB=l?&zl8#ce?$(6tfV4;n|$Q#M3KcmrqCrggZ335I9N&KgK2!SNs@oTrSOZW8|-I05R) z2|{LFX=28^casd-@-Y%u3XeQH;gcRovV4GJJ4cW*+Vfx(%?pEII=-elniEi|rRU3+(NNR!BlLwl^b#cAO#Lf_0Rh#>AAf<`+GEa{1G zv{#l2EU3tT*Ed;|eMJpBp9&3Gw185}7A@Qvsgl|@N#8-@WXV26EvbL90P$UXg0;AY zX;M*HnV5(SgPa~}0aBQ-7WiieSLBMgok7WhV5GDfbb_=dY8R+Vs*sfByRq#@y6 zccTE~Fj251){-dj&*(K^z)irvMKLqr%5xQbBBiT1bzipPi*QWtL}>ZPGF@DzAmu89=QxMj{H-M$iizVc(_7jtN+$vVm`J^BdBt#~bn2guhkzTb)+# zZEqx}iJVpBtWHB7kmetU0EN1vQGPeESs{(8b;u-bB&+oUgp%g##1Vi#qe;ljpTr-L zua}%3!vQNra}9P+{XN~`v@zIAWEymIHSyt_s%us&FaZR92UxILy$I;uvrH_Wz?fmfTO z&Q~|3>KZ32-rgR!ltv#XYbF>n`R{^Vv-mN^ig)vvKbF$CX3^)j?VNWX|9Oabtn
X6n=9e{`TE6h@7g0U8bp0_H^cH0n>&g?R8YMHJIr@AiZX!a5k_z&+bO8# z9lKgVmlfXYg`8;F*)_HFw|GK_%c0U>Mp%8yFzk{=oeG^%nD6Kue1(cw^ zFNP+EF^IetNJpE)S^z_LUkv?yF${uHsn0Rp0nTq~gDrVDBw0^t1TlCttpRO03MDfp zJWSDh((0o<&xsh3ocIbk{{ROjoBY{)M$oXKCvftp5O`#;V&UGCQAVlrNUuN4k=$bI z=+?1nOwYYape9<8#1uXC=D_8F*df>pE?$watWdtrLR~me6Rk;FDzN-{Ol$|zj`65| zE_g1j7jZmA41orM8nU4&CQ+8O=2+iA4-Hz0=VyaewuTblpnC6xGs_}M0@JrYpc>IW zhYiOY?$VEGL?#WJZ>jWF69jn9A_(x96sMFctQ^s$%+8C=qs?Q%=>DYH7keyeu1S@a zkF<`h8QqgATMW-bFqzT=m#EO9xTfTDCJQ~^xLL)&Z-bLopE=$)Yf8J2KchQzR^w&+ z%MOY5z}lB*c0*NpdrwfEFwH*p+^wXSY`|Nx)_pk}Cnzauk)lDeM9NFi`0q~HxZDQ`Yped9%ZGtUkQ|2-zyE)Yp_liyu`hLkA0N!v2yOcJZrP!1#&7;YVgAG1u(LTyBD5V z03(}5wzQ;8n)4_%hIFiSfpoGtJy<$#sPAP}hkBP`9F#AR*J8n^P)uM|EPz#^w2oXN z4{$3Nz^znpMc4f>&<0E1P@#mBUeyBFRrgIVi-Gj2v+32~Bts321f;zb{#x=2{sAzP z!ZLX@EEnn)$TfRtdf2@HMuSo=VFep`Dq&^s_62Zrsi_2e@0ta$vMIi>WC4t9I@ysy z>-?NY8DUgMT9JiDa?uJ+XoXdBi>!`lMJvKyp9{~Kz#N7l+X>+*1#=iscD zaW>@PY@Ce)s`d!5ij~~(W+_ss@^c29r7ppNIDxB`<~WjDfmWSlnm+hqS<`1i{Geemy-{g1={xa{8#|9;tj0RDE_ ze-QqIvi}hLha~^^c=%Zl;LXZNWq_ZRQTDUC%6?W?+0W`K`&mA+pS3*FV69H8qYP&~ z3pnf5JUDB6z@@g%;j=a%JbYtqK>jc(IoSi~6I6@rsQ2KC2KnCtqxDPC!v)g#QnYY^ zG_q@*@Fbh5hjZo+CmeG1@?Iy1Z=UlAPu-KFI7}@gJWcMs@MJm65yD%btl702GvgVU zG|*NC56xxtfJDQdy<+(uKctUn<~xZS$uoEu(_B~u6BCb=21?k0{h8+ORU0Na2H`8u zfoBp=Ml|Ac2nE;)W|a^?&R>ZbXe8sQM668iK(I~*^JHNjw1)D%4EwVNeu!UZxjawi+|uZNv-}(=@g1mbI5$Uf-oCrrZr>* zLuTPHznO~v(06p8yGQ5{apgl$>>#OXuvV!QTC3St$+R++giOZbEZ9`ZvSG@nSvwFl zMmQ5HfN7T{9}p@)w9BM}vV@h<7fn`p;827IZ>vQb%uIUK#>B(W#KZ~mcX0A$1!4k# zK@XK|9?`&`WAM$8q=D_laHz{_`%Uq-vCXMM*VvKqO|ihW!lid`7^UFj;%%SL5b-$g za`SVfY~p&r+nBEE_g}-Gvb33Q?7zbM;8pueR89PEh$8-XIDP}|R=41)1v(tK1p)0g zi1UOrzYTtUYoBQdKKi7@R0ojS zpqZU}R6zR(WRnxCmrI~(-6CQ$!-h=vyG~Ir9nejmVH6wdrr+{Um8gQOTr{c-~~Ib00n3)LXbd1In9gW|B1IOMi`dV zqW2umze1mfHX+XMdoYqrYZ)<%LQsr(j6lN`N=z-*i z`FP&|CNCHNHYgU(#(?6$d@rr<4D^Qs-MF%#JG2G&*5LG5E2KqT!L;spu)FJc_zShK z^JH3gh6M)IzUR8q`tHyHB4|*1g!ZzSdwX$R436Ip&I?KVrTG;QjUVp|!!7~fRT3C4f~{h(MhS)&!63pv)+PRm8cr9$ zO+FBUPg4Nrii3e3F~$5xA*_5Zt%J<$Pomb&}9_^ zy(oZWI3fN&NNUj7(tKneD$o~VhLdSMt{S4&V!A0bQ-5!l-vaSie>ao!L9!nXHJ)Ox zCh4_GdaagTjmM>L$I}K%rjbSv^EEMFKV=1z4I-9^zn&PlZ)Q|>Q!%Y z#g1>CddvU$3=|h^FJ{mA= zps*1lmJy7G0~-cn1qCa~pF_Im^juAcq!_&!{=Y5FEcwY7>&Q zr?p%90-dn&B!;9dt=$_w9u#Nw0+9mZ;F<9MkN8X6C_f*$TR;k(ClSv{n;}V7w~5p_ zpLN1vLSl3a^+?)Eq`U)nmGp_YBS`8Xne=^v?}P3+E4eHtCT5$RU;f=NH+FK`y_EEw z{P(qQYbTzYYD(5Va#eNRy@d&Pp82HCuL4bFGhAjmU_=IDok@!?CxjQL1|SV zW=Vel7N^6 Z$02Ll9QHw3~@5JUq(G42TvVObm$Zvx_Xioc-``u@N<7%d6X^3*{} zF}CQzSQd@lA@-dKN$cOg5VjiOw1rV8`P6>ea;&?j2dowY!p13h5{qFt*ugU7JD{VZ zTLLH{R-?2z&jN)l9pYo?eWoCnSy<)u=%dnVvKu)w3zjzs@gJZO|6E!*+?>HY(sll; zHwxUhRN#s}Ev||kN)*>kz*#-{WTNQe1^ zZL~RAP;;%IZlW@yrVlqQ&au^(){d`@?oAffWwZo?c`R-oaJ}ih?1i?cxFMrwP#g^` z9Ba9>eSCZLS=;=KcFQqK%0EuAx3yzg*oSHUekSH2YWbxJF0%HxeX$HSKf z#|K}FOh7sQwqde7S^Ma<+SaR&O)uS(Sh^>GjVf)<2V6-w|*AO572cc622i zUDqAmXcBi#W;x5ZfsROqFP$Af8#7NFOnO#jRxtQV&RQV1<<{3WfenrYGL7s@6K5@& zZ295#_qIIXqx!%!chatuVDN_duL9%hiW z=%HvZwkuJ-aS$&i`_zn2DaTsW!&@p9J?W{7dse5srEeN88)9Wi?~2K_ z*Sza*QJZ0WmipdYdwFeaS8~zH_@cEiPc2@AZtzvTdG_+z32SokL-ECrfk!H@eDlf6 zPsX0ZAZ`0@ zR5=vm)?Nv;U&T4yW5>tssRB>bFuvlJns-*-l>J(jvl2tK(96E))d+zEc%%C9hFc&5 z6&Rm#$gD-kLLpi8gzCp*l7T`;4W~X5nvak zNlkgH$mFa7kws89UVqDDcKTpMUF3^tW5?gJCtWL3Mee!(i_5aa^BRbM<+OKw!n;1{ z-I$WUEK3&GUn^cUSupKwPI#MPqd~>* z^xn%)rOI&e%(b$H%lzbHQ*{YX>xkusw>0`x?9}BW(`5~bvW6rMYI_?;te>LSBfoN1 z!G7LbGP2_nPt}+@Rp^eFY)lkxOjR!%+Y#;jW*dyzGn}Wa;1=g`7i3m)HA^M<&1%_` zSZ+9y5T){9H2%i zgYy&17M!9e)D7>#P_lU98u#b7pVwfi`8-5=-j}x6+Sl`cTd-N({*dVxMf++Hn#JM~ zllmh5X4b-a5F?a#)mfn)Oi*N_5zPWdBeZwSa_{vL_dXv7nQt`0$0ewt zeacCPR)F(ome4qYmE-)0;cjnZJf`@MD9YVD|Kfb{**robiV<3esEUxtX!=c2@fe@7 zJ1!244vamWv{%HQyk@VTtoUK|d(~6sWK+vWnncrXNRF(wkBf?BYs6J+W;Z*DCm3f< zl8{SzNAST*6SF?&$=QG_VuyE7HF_j|nj|J{w+>eNRq~T8LHrm_p6!LmnCS8aYAiF; zXQ{$uz%zDi|H26~Ry$O}D^y)!(t|=>!lchs7J$c;q(omf*gx!0E0I5*e7t zBZ&>tX895@$RS0X4wG&K>*xs{3wP3vMR`b{M{PON1NUVIE-B|<;$Cqu>9p!4!;ntJ zjXDup$=8&*C~%u)2)sBiXCmk)D&mjJR!-MkpS@3K+~B$=-YlSfPx^(vFy< zbo1i4!dZKgh&5uPT$S1(cHUj5xm#iZcjv`YNxGiASl(G**|GS%uO+j-E+TDL-q+Gu zU(4ovEsvt9mlU_g`Btvh;C2hh4@sg^P9?;_o;3>!iHs2A}sx-9s1pkg7w`Y}6wm7V!`0gcEc# zuSzLHD?*}5I`kC(kflL78YqqEG?aW(k9-{$K$E0Rg3d-NLgIWjf>K_JDP5*RXebJ0 z_Hqtw3c=j5t3SXA<3E%dfGMdyB-ttMCnAN_PoxS#W%UE3IW83+C#hI#;9xRzS1dCs zilk&ht4a459o&KYhh9c1XbY+*9jKPnV#%NTE`EY7Y%LfNvk$)IF6m-N*yO1QH81fX?8!UAmbMG`pt#cnw) zbdQ~S?V*u9zr*p+D?8%$8g^aY$A!{$dGdiSla>31eX;sa_{HkZY!#3QRf1K#9UhXE zfi|jgRc4{=yu#ggu(%anIlDNrz8eH}iQw2(ZjNLAs@_l?faNb{m8R`80cofC>Mv9) zt8p|yD?+p_z!+RZV(PSpM9*ReX5K8dLE0vtdV`Xs6H1^#|2^7fHtHp6p@)$tBx@fR z;oQJ$&q8$|eCOE_VMOzB zP1p=}0Z}AH0^^FHGLnOY$ppd-<1xC-lSxP!1`9Hb4ahcaL|WUKZi22V<-{Z8&;cI| z#VYCu-9SIP#frud1b=kuOMHruY~&nP(4~lUUPZiTQ?MTkfQt2KS`UdodBi6vN!FuI z_=uk7KawCX?Yyf$MeNIQt^%D$vTJnS?OKht=#E9DeU!gt;9RBAmN&Ow-X41*Sz4bg zY;C}!`nW-8+21+czM?g zyE3W*BV>F9V*_Jd(Z{0$QMwp8qXzKiT%Z;}67Uty#?=!T5S8QU9cgn;4T zThGMy#tT+wjPw~ir{}UQHZT!PdRJ%61hUKmSqWsD1+o*!0V%H z%Q)#%0fZypS~Yei=1F*JlZF0FAwgY;06u!@Ydx7F3cHc!6?L>V)_8fx>$Z%CU|!C; z=*pIuE_UiI-##=O3qqx zWmWX~>z}yR^EQI?h%w!rv$;8WW<8<6W=Sjc7(5e?<=ol8vqvusDWyvFUpk zb!RMfGX~ft4(7K!{y^ryBy?iP;qtPBv<&pcQrIm9%fO}K!ms$|!ExPj{RAs=m}eBHgi zre48!JkWEja$!b6;jn7+(w^*)3f+;<*Ck$U1p6i%|6 z_>2s)L{fV*sxHxT6Dg+UAD75qqO+TB2p_qZjX26NzmOqMarKIE0i3+U97C#6BStH7 z5{m^b4xwd7*oYB^P&$}*V}2-h1@9nldWJNDen{K92XPH7?y@=Yj0?sy4QUa}%;pDf z0un6G9YNkOViT+|?ov}3dMgAC+;YKoS9$CUrt6?o$vlm202#wGA9T{pTO2< zaSRP2(xS^ca2Cm-PFsb)nKn3ih8(46LI|MdB{$%ul!A_`$odWhr#P!6E?2a~Z)-#v z2~rVAR3(Xu-j-}I*u^poVHP_j85OM~sY3@%mX2csy`4GcQjtcGi4+c=-hCp8uIguI zN0OaJ+_Uek4bi}4;}6%ow`R(FwK2JB>qqY7s=ZhEw>C&xr3-wY^<;5YdrC5 zXwlzmgMEw!3;9tkrNItL4K~I9O#7jB>i9=|a_!FRtJ*(0_N!Ixtku|2t`~g`{C7OGZXLMs$TmH|FIBAKSXT*t6ZAd!r-+w#(h6vCwrT+q)LE~N+ zh!9El4H~nQebAtI)2g;U@!!y@gT^emm-$+82jb(F`M_DfDNnpcDr_inI5t^xGB}{6 zwUU-vubcEV6Dv-G;xif)I2-0% zbg6v2JZ6}vNIL6qX3FNh*gM)AJs8`ZwAGBLZ8l`IC23|SLzF}}BJqMGngY4VLHe6R9b-wa^G!zrQ{rtqhlw3$YiQ|yjd6O!c8)ui2>%l zXNz@VuFJ$rNb0B5Ki@~Ou(x(_Q08w>*`34N|H)ZAqD`3{?5dwbNpopTl{8nT?5>OF zM$bh{lJ=?z{+ivNa(LOnTfq)1A2VjIpd@4SXiMIA&2V_# zYT_)elzq`nXK}Q0ymWZy$L^}wlM{nUcQbC;Y8}}&H%x=$VesSI;^yUX&GNfhUZMuH zpwCR8T6Fp|?>sv!`k6fvyBIgi+FBw!PE6UQlkI|fNR2;G(IGAVba^}etS$pfrwR=8 zTBJtSJ8yHn;?(KvMxA3Z)Wlc(;Rt(g`Enr4w>iR+o||kbg)Z=B<9F&X?1(babASSnWKG8ugWAxI`9h?M%|qDp{|o z0T2V^z`B8BhGcAQ#bWz&iUnH+7+$3fB$b2MB*3hcU?|0oeuv1amwrKoWOXqAR6?v+ zFq)ztUtxM6i{zCxLF||26e}rq@Rk)0s}_qOC7E{QC(UF<3Pf%I$b^{OhKX!QcoTA< z#Mz}v_As-lbOA|QsQev}l`-*RNc;|>%n-%f2hT8f%f(ed(6RF^LsT=-GPx*Gw-JVd zbvs@O#|*>U;V$o1%266)`g+{F2VFXJ(B~4)irB$~b6NhUG0RPZbL>!b>-aMXL*T)tRcqOf6j_ivh8FRU3Wjj0pPC9$4_vngp_6*sM(+?+5x z^id0h0@@-7j@EDE9{_93X8uBOY)8zJaMeu&uG>~#4M616-1@Deu?W)kG)->4?tJK@ z&GeIACGpa_iJtg|-Er$4Sbdob##*C0lVA_H4Pkp6;?Xx zqXDwGjQ@$GDPRzMAQ|ESbzXdeZFMKHIVQ>V2ZQEgVle1~DIZMlfOwn`eLzf%%tj=g z1DkBJs$kHb7tsgtAtIA}lI7A=FVB6*$yqLjrg~|x0jpcdVlLKQSy+@S9%hNF7K*0qrg`jz%tIW*G`e%nk81$d?4e1Lc(OEFBQ! z7VKaUei((MCLgdkK=);fc{e&vz|a90NF~_abMYx95?!GqnXbPHRYz9OM8CXulw|8v z|3!p@O#}Vz0xLca&Ct;pEEF)FCWCImDkUs7@Wz?4CChI5O0NsjOdOIcb=cH}4!&IUm4@)+)d zeU8-<0|{Z@Q36nTmm%&K>g6ly#lL_LfRlJhn55?SL2eHqY|&((@iAJSeEZrD0DLM7 zwq(I|zG{5w&3-W+ptVDe(3b^UvS29!Oxxrf$f&?4EhR|kNEWWx6tJBUzL3ygu9D0~ z5AMRMu-Z#7;HelD&mp0pM&)hicW7F)9O-##3 zoGlVF$b2ed3UbUtX~OR!GKRNN@ie8)(r=S>&}F0Sh=BM3)$y<3uwOb1 zv&Ny{JcOv6e!=i$5CEy|K`TPp!@b4aQ`_hAAOw-7?&I<1WajoA!JZOm1pVXuvU#)yGOF zhCZ!ro_gqNZDRedWbN*Rrh0hC$k}M=jd`J;+7_n_wvjWVc9>A63=Xiz?0*rCGT3#) z+hAR3_uUf%`o6-V;a#8D-MACh?j8$`Mo3yCdu|pM;dfxk`7hP05|)aQUD3j5XUbj@ z?S#Q#s=9V!Nwjv%@Uf!^clXq+hCgyUgYsc}S4B*K(7;WQmd?RvV8idYFaaS1sj2oZgSnSab3K7}2w246}91>4a z+~>$SLk>Hka+G|1={LotyXQ$SFJ2&L4LSSC z`9qY9o!t=+Q?QGim&iFz&a32%lT$^`v*di6oHxn&4mp2D&YzP*T#|Gf<9{X}-R~q> zJ(Bi4kD~mLf;2Ixj~Gqqpb6G15y7V^+G;py!-kW60#55aBHjQp{&S(V=nvc`%=5qI z7X6xA_6cYIHCOft=lFy(f5KT1{Diapnk)W0s}2i>8;axZ$j@)>Pv<-H;vYdi$)hAeZBeR zb1$45*5rAr5i;6_bvNzJDev)=qbFr~G-dBhm7GfzK9+Jml5(}&Dl{2X!y3|Ndq=ij z+%>xE)jh+;n>NR=`KGmW~GAs;LhM)H}sf})I>d^q9Y&REH3<8-DOJAAhvQ#J8*w@>iP`K7my@YMyXVOs{0 z6s2tzCHc}@K=DvP$};53VDGE3j0|2=7pFA#;ca9(GG>cCHQ9c(>Q|a=kXL9*m?-!E E2W", + "videoId": null, + "id": 921426 + }, + "comp": { + "id": 1202269, + "compType": "RoundRobin", + "name": "Regular Season", + "abcName": "VTB", + "level": 5, + "season": 2026, + "tag": null, + "logo": "", + "isActual": true + }, + "league": { + "id": 1202263, + "compType": "League", + "name": "VTB United League", + "abcName": "VTB", + "level": 4, + "season": 2026, + "tag": "vtb", + "logo": "", + "isActual": true + }, + "gender": 0, + "status": { + "icon": "fa-regular fa-circle-check color-darkblue ", + "id": "ResultConfirmed", + "displayName": "Finished" + }, + "region": { + "regionType": "Capital", + "timeZoneId": "Russian Standard Time", + "name": "Moscow", + "countryId": "RU", + "id": 1 + }, + "arena": { + "shortName": "Megasport", + "name": "Megasport", + "regionId": 1, + "id": 11704 + }, + "team1": { + "teamId": 15, + "start": 11, + "label": "", + "abcName": "CSKA", + "name": "CSKA", + "regionName": "Moscow", + "shortName": "CSKA", + "logo": "https://files.infobasket.su/logos/13842.png", + "arenaId": null, + "id": 450736 + }, + "team2": { + "teamId": 1390, + "start": 10, + "label": null, + "abcName": "Uralma", + "name": "Uralmash", + "regionName": "Yekaterinburg", + "shortName": "Uralmash", + "logo": "https://img.infobasket.su/logo/19664.png", + "arenaId": null, + "id": 450735 + }, + "ot": "", + "distanceIndex": 0, + "period": null, + "timeToGo": null, + "timeIsGo": null, + "hasCoefficients": false, + "photoUrl": null + } +} \ No newline at end of file diff --git a/get_data.py b/get_data.py new file mode 100644 index 0000000..b20029c --- /dev/null +++ b/get_data.py @@ -0,0 +1,896 @@ +from fastapi import FastAPI +from contextlib import asynccontextmanager +import requests +from datetime import datetime +import threading +import time +import queue +import argparse +import uvicorn +from pprint import pprint +import os + + +# передадим параметры через аргументы или глобальные переменные + +parser = argparse.ArgumentParser() +parser = argparse.ArgumentParser() +parser.add_argument("--league", default="vtb") +parser.add_argument("--team", required=True) +parser.add_argument("--lang", default="en") +args = parser.parse_args() + +LEAGUE = args.league +TEAM = args.team +LANG = args.lang +HOST = "https://ref.russiabasket.org" +STATUS = False +URLS = { + "seasons": "{host}/api/abc/comps/seasons?Tag={league}", + "actual-standings": "{host}/api/abc/comps/actual-standings?tag={league}&season={season}&lang={lang}", + "calendar": "{host}/api/abc/comps/calendar?Tag={league}&Season={season}&Lang={lang}&MaxResultCount=1000", + "game": "{host}/api/abc/games/game?Id={game_id}&Lang={lang}", + "pregame": "{host}/api/abc/games/pregame?tag={league}&season={season}&id={game_id}&lang={lang}", + "pregame-full-stats": "{host}/api/abc/games/pregame-full-stats?tag={league}&season={season}&id={game_id}&lang={lang}", + "live-status": "{host}/api/abc/games/live-status?id={game_id}", + "box-score": "{host}/api/abc/games/box-score?id={game_id}", + "play-by-play": "{host}/api/abc/games/play-by-play?id={game_id}", +} + + +# общая очередь +results_q = queue.Queue() +# тут будем хранить последние данные +latest_data = {} +# событие для остановки потоков +stop_event = threading.Event() + + +# Функция запускаемая в потоках +def get_data_from_API( + name: str, url: str, quantity: float, stop_event: threading.Event +): + if quantity <= 0: + raise ValueError("quantity must be > 0") + + sleep_time = 1.0 / quantity # это и есть "раз в N секунд" + + while not stop_event.is_set(): + start = time.time() + try: + value = requests.get(url).json() + except Exception as ex: + value = {"error": str(ex)} + ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + results_q.put({"source": name, "ts": ts, "data": value}) + print(f"[{ts}] name: {name}, status: {value.get('status', 'no-status')}") + + # сколько уже заняло + elapsed = time.time() - start + # сколько надо доспать, чтобы в сумме вышла нужная частота + to_sleep = sleep_time - elapsed + if to_sleep > 0: + time.sleep(to_sleep) + # если запрос занял дольше — просто сразу следующую итерацию + + +# Получение результатов из всех запущенных потоков +def results_consumer(): + while not stop_event.is_set(): + try: + msg = results_q.get(timeout=0.5) + except queue.Empty: + continue + if "play-by-play" in msg["source"]: + latest_data["game"]["data"]["result"]["plays"] = msg["data"]["result"] + elif "box-score" in msg["source"]: + if "game" in latest_data: + for team in latest_data["game"]["data"]["result"]["teams"]: + if team["teamNumber"] != 0: + box_team = [ + t + for t in msg["data"]["result"]["teams"] + if t["teamNumber"] == team["teamNumber"] + ] + if not box_team: + print("EROORRRRR") + next # log + box_team = box_team[0] + for player in team["starts"]: + box_player = [ + p + for p in box_team["starts"] + if p["startNum"] == player["startNum"] + ] + if box_player: + player["stats"] = box_player[0] + + team["total"] = box_team["total"] + team["startTotal"] = box_team["startTotal"] + team["benchTotal"] = box_team["benchTotal"] + team["maxLeading"] = box_team["maxLeading"] + team["pointsInRow"] = box_team["pointsInRow"] + team["maxPointsInRow"] = box_team["maxPointsInRow"] + + else: + latest_data[msg["source"]] = { + "ts": msg["ts"], + "data": msg["data"], + } + + +def get_items(data: dict) -> list: + """ + Мелкий хелпер: берём первый список в ответе API. + Многие ручки отдают {"result":[...]} или {"seasons":[...]}. + Если находим список — возвращаем его. + Если нет — возвращаем None (значит, нужно брать весь dict). + """ + for k, v in data.items(): + if isinstance(v, list): + return data[k] + return None + + +def get_game_id(data): + """ + получаем GAME_ID для домашней команды. Если матча сегодня нет, то берем последний. + """ + items = get_items(data) + for game in items[::-1]: + if game["team1"]["name"].lower() == TEAM.lower(): + game_date = datetime.strptime(game["game"]["localDate"], "%d.%m.%Y").date() + if game_date == datetime.now().date(): + game_id = game["game"]["id"] + print(f"Получили актуальный id: {game_id} для {TEAM}") + return game_id, True + elif game_date < datetime.now().date(): + game_id = game["game"]["id"] + print(f"Получили старый id: {game_id} для {TEAM}") + return game_id, False + else: + print(f"Не смогли найти игру для команды {TEAM}") # DEBUG + + +@asynccontextmanager +async def lifespan(app: FastAPI): + global STATUS + # -------- startup -------- + # 1. определим сезон (как у тебя) + try: + season = requests.get(URLS["seasons"].format(host=HOST, league=LEAGUE)).json()[ + "items" + ][0]["season"] + except Exception: + # WARNING + now = datetime.now() + if now.month > 9: + season = now.year + 1 + else: + season = now.year + print("не удалось получить последний сезон.") # WARNING + + try: + calendar = requests.get( + URLS["calendar"].format(host=HOST, league=LEAGUE, season=season, lang=LANG) + ).json() + except Exception as ex: + print(f"не получилось проверить работу API. код ошибки: {ex}") # ERROR + exit(1) + + game_id, STATUS = get_game_id(calendar) + + # 2. поднимем потоки + + threads_long = [ + threading.Thread( + target=get_data_from_API, + args=( + "pregame", + URLS["pregame"].format( + host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG + ), + 0.0016667, + stop_event, + ), + daemon=True, + ), + threading.Thread( + target=get_data_from_API, + args=( + "pregame-full-stats", + URLS["pregame-full-stats"].format( + host=HOST, league=LEAGUE, season=season, game_id=game_id, lang=LANG + ), + 0.0016667, + stop_event, + ), + daemon=True, + ), + threading.Thread( + target=get_data_from_API, + args=( + "actual-standings", + URLS["actual-standings"].format( + host=HOST, league=LEAGUE, season=season, lang=LANG + ), + 0.0016667, + stop_event, + ), + daemon=True, + ), + threading.Thread( + target=results_consumer, + daemon=True, + ), + ] + threads_live = [ + threading.Thread( + target=get_data_from_API, + args=( + "game", + URLS["game"].format(host=HOST, game_id=game_id, lang=LANG), + 0.0016667, + stop_event, + ), + daemon=True, + ), + threading.Thread( + target=get_data_from_API, + args=( + "live-status", + URLS["live-status"].format(host=HOST, game_id=game_id), + 5, + stop_event, + ), + daemon=True, + ), + threading.Thread( + target=get_data_from_API, + args=( + "box-score", + URLS["box-score"].format(host=HOST, game_id=game_id), + 5, + stop_event, + ), + daemon=True, + ), + threading.Thread( + target=get_data_from_API, + args=( + "play-by-play", + URLS["play-by-play"].format(host=HOST, game_id=game_id), + 5, + stop_event, + ), + daemon=True, + ), + ] + threads_offline = [ + threading.Thread( + target=get_data_from_API, + args=( + "game", + URLS["game"].format(host=HOST, game_id=game_id, lang=LANG), + 1, + stop_event, + ), + daemon=True, + ) + ] + + for t in threads_long: + t.start() + if STATUS: + for t in threads_live: + t.start() + else: + for t in threads_offline: + t.start() + + # отдаём управление FastAPI + yield + + # -------- shutdown -------- + stop_event.set() + for t in threads_long: + t.join(timeout=1) + if STATUS: + for t in threads_live: + t.join(timeout=1) + else: + for t in threads_offline: + t.join(timeout=1) + + +app = FastAPI(lifespan=lifespan) + + +def format_time(seconds: float | int) -> str: + """ + Удобный формат времени для игроков: + 71 -> "1:11" + 0 -> "0:00" + Любые кривые значения -> "0:00". + """ + try: + total_seconds = int(float(seconds)) + minutes = total_seconds // 60 + sec = total_seconds % 60 + return f"{minutes}:{sec:02}" + except (ValueError, TypeError): + return "0:00" + + +@app.get("/team1.json") +async def team1(): + return await team("team1") + + +@app.get("/team2.json") +async def team2(): + return await team("team2") + + +@app.get("/top_team1.json") +async def top_team1(): + data = await team("team1") + return await top_sorted_team(data) + + +@app.get("/top_team2.json") +async def top_team2(): + data = await team("team2") + return await top_sorted_team(data) + + +@app.get("/started_team1.json") +async def started_team1(): + data = await team("team1") + return await started_team(data) + + +@app.get("/started_team2.json") +async def started_team2(): + data = await team("team2") + return await started_team(data) + + +@app.get("/game.json") +async def game(): + return latest_data["game"] + + +@app.get("/status.json") +async def status(): + return [ + { + "name": item, + "status": latest_data[item]["data"]["status"], + "ts": latest_data[item]["ts"], + } + for item in latest_data + ] + + +@app.get("/scores.json") +async def scores(): + quarters = ["Q1", "Q2", "Q3", "Q4", "OT1", "OT2", "OT3", "OT4"] + score_by_quarter = [{"Q": q, "score1": "", "score2": ""} for q in quarters] + full_score_list = latest_data["game"]["data"]["result"]["game"]["fullScore"].split(",") + for i, score_str in enumerate(full_score_list[: len(score_by_quarter)]): + parts = score_str.split(":") + if len(parts) == 2: + score_by_quarter[i]["score1"] = parts[0] + score_by_quarter[i]["score2"] = parts[1] + return score_by_quarter + + + +async def top_sorted_team(data): + top_sorted_team = sorted( + (p for p in data if p.get("startRole") in ["Player", ""]), + key=lambda x: ( + x.get("pts", 0), + x.get("dreb", 0) + x.get("oreb", 0), + x.get("ast", 0), + x.get("stl", 0), + x.get("blk", 0), + x.get("time", "0:00"), + ), + reverse=True, + ) + + # пустые строки не должны ломать UI процентами фолов/очков + for player in top_sorted_team: + if player.get("num", "") == "": + player["pts"] = "" + player["foul"] = "" + + return top_sorted_team + + +async def team(who: str): + """ + Формирует и записывает несколько JSON-файлов по составу и игрокам команды: + - .json (полный список игроков с метриками) + - topTeam1.json / topTeam2.json (топ-игроки) + - started_team1.json / started_team2.json (игроки на паркете) + + Вход: + merged: словарь из build_render_state() + who: "team1" или "team2" + """ + if who == "team1": + payload = next( + ( + i + for i in latest_data["game"]["data"]["result"]["teams"] + if i["teamNumber"] == 1 + ), + None, + ) + elif who == "team2": + payload = next( + ( + i + for i in latest_data["game"]["data"]["result"]["teams"] + if i["teamNumber"] == 2 + ), + None, + ) + + role_list = [ + ("Center", "C"), + ("Guard", "G"), + ("Forward", "F"), + ("Power Forward", "PF"), + ("Small Forward", "SF"), + ("Shooting Guard", "SG"), + ("Point Guard", "PG"), + ("Forward-Center", "FC"), + ] + + starts = payload["starts"] + team_rows = [] + + for item in starts: + stats = item["stats"] + row = { + "id": item.get("personId") or "", + "num": item.get("displayNumber"), + "startRole": item.get("startRole"), + "role": item.get("positionName"), + "roleShort": ( + [ + r[1] + for r in role_list + if r[0].lower() == (item.get("positionName") or "").lower() + ][0] + if any( + r[0].lower() == (item.get("positionName") or "").lower() + for r in role_list + ) + else "" + ), + "NameGFX": ( + f"{(item.get('firstName') or '').strip()} {(item.get('lastName') or '').strip()}".strip() + if item.get("firstName") is not None + and item.get("lastName") is not None + else "Команда" + ), + "captain": item.get("isCapitan", False), + "age": item.get("age") or 0, + "height": f"{item.get('height')} cm" if item.get("height") else 0, + "weight": f"{item.get('weight')} kg" if item.get("weight") else 0, + "isStart": stats["isStart"], + "isOn": "🏀" if stats["isOnCourt"] is True else "", + "flag": ( + "https://flagicons.lipis.dev/flags/4x3/" + + ( + "ru" + if item.get("countryId") is None + and item.get("countryName") == "Russia" + else ( + "" + if item.get("countryId") is None + else ( + (item.get("countryId") or "").lower() + if item.get("countryName") is not None + else "" + ) + ) + ) + + ".svg" + ), + "pts": stats["points"], + "pt-2": f"{stats['goal2']}/{stats['shot2']}" if stats else 0, + "pt-3": f"{stats['goal3']}/{stats['shot3']}" if stats else 0, + "pt-1": f"{stats['goal1']}/{stats['shot1']}" if stats else 0, + "fg": ( + f"{stats['goal2']+stats['goal3']}/" f"{stats['shot2']+stats['shot3']}" + if stats + else 0 + ), + "ast": stats["assist"], + "stl": stats["steal"], + "blk": stats["block"], + "blkVic": stats["blocked"], + "dreb": stats["defReb"], + "oreb": stats["offReb"], + "reb": stats["defReb"] + stats["offReb"], + "to": stats["turnover"], + "foul": stats["foul"], + "foulT": stats["foulT"], + "foulD": stats["foulD"], + "foulC": stats["foulC"], + "foulB": stats["foulB"], + "fouled": stats["foulsOn"], + "plusMinus": stats["plusMinus"], + "dunk": stats["dunk"], + "kpi": ( + stats["points"] + + stats["defReb"] + + stats["offReb"] + + stats["assist"] + + stats["steal"] + + stats["block"] + + stats["foulsOn"] + + (stats["goal1"] - stats["shot1"]) + + (stats["goal2"] - stats["shot2"]) + + (stats["goal3"] - stats["shot3"]) + - stats["turnover"] + - stats["foul"] + ), + "time": format_time(stats["second"]), + "pts1q": 0, + "pts2q": 0, + "pts3q": 0, + "pts4q": 0, + "pts1h": 0, + "pts2h": 0, + "Name1GFX": (item.get("firstName") or "").strip(), + "Name2GFX": (item.get("lastName") or "").strip(), + "photoGFX": ( + os.path.join( + "D:\\Photos", + latest_data["game"]["data"]["result"]["league"]["abcName"], + latest_data["game"]["data"]["result"][who]["name"], + f'{item.get("displayNumber")}.png', + ) + if item.get("startRole") == "Player" + else "" + ), + "isOnCourt": stats["isOnCourt"], + } + team_rows.append(row) + + # добиваем до 12 строк, чтобы UI был ровный + count_player = sum(1 for x in team_rows if x["startRole"] == "Player") + if count_player < 12 and team_rows: + filler_count = (4 if count_player <= 4 else 12) - count_player + template_keys = list(team_rows[0].keys()) + + for _ in range(filler_count): + empty_row = {} + for key in template_keys: + if key in ["captain", "isStart", "isOnCourt"]: + empty_row[key] = False + elif key in [ + "id", + "pts", + "weight", + "height", + "age", + "ast", + "stl", + "blk", + "blkVic", + "dreb", + "oreb", + "reb", + "to", + "foul", + "foulT", + "foulD", + "foulC", + "foulB", + "fouled", + "plusMinus", + "dunk", + "kpi", + ]: + empty_row[key] = 0 + else: + empty_row[key] = "" + team_rows.append(empty_row) + + # сортируем игроков по типу роли: сначала "Player", потом "", потом "Coach" и т.д. + role_priority = { + "Player": 0, + "": 1, + "Coach": 2, + "Team": 3, + None: 4, + "Other": 5, + } + sorted_team = sorted( + team_rows, + key=lambda x: role_priority.get(x.get("startRole", 99), 99), + ) + + return sorted_team + + +async def started_team(data): + started_team = sorted( + ( + p + for p in data + if p.get("startRole") == "Player" and p.get("isOnCourt") is True + ), + key=lambda x: int(x.get("num") or 0), + ) + return started_team + + +def add_new_team_stat( + data: dict, + avg_age: float, + points, + avg_height: float, + timeout_str: str, + timeout_left: int, +) -> dict: + """ + Берёт словарь total по команде (очки, подборы, броски и т.д.), + добавляет: + - проценты попаданий + - средний возраст / рост + - очки старт / бенч + - информацию по таймаутам + и всё приводит к строкам (для UI, чтобы не ловить типы). + + Возвращает обновлённый словарь. + """ + + def safe_int(v): + try: + return int(v) + except (ValueError, TypeError): + return 0 + + def format_percent(goal, shot): + goal, shot = safe_int(goal), safe_int(shot) + return f"{round(goal * 100 / shot)}%" if shot else "0%" + + goal1, shot1 = safe_int(data.get("goal1")), safe_int(data.get("shot1")) + goal2, shot2 = safe_int(data.get("goal2")), safe_int(data.get("shot2")) + goal3, shot3 = safe_int(data.get("goal3")), safe_int(data.get("shot3")) + + def_reb = safe_int(data.get("defReb")) + off_reb = safe_int(data.get("offReb")) + + data.update( + { + "pt-1": f"{goal1}/{shot1}", + "pt-2": f"{goal2}/{shot2}", + "pt-3": f"{goal3}/{shot3}", + "fg": f"{goal2 + goal3}/{shot2 + shot3}", + "pt-1_pro": format_percent(goal1, shot1), + "pt-2_pro": format_percent(goal2, shot2), + "pt-3_pro": format_percent(goal3, shot3), + "fg_pro": format_percent(goal2 + goal3, shot2 + shot3), + "Reb": str(def_reb + off_reb), + "avgAge": str(avg_age), + "ptsStart": str(points[0]), + "ptsStart_pro": str(points[1]), + "ptsBench": str(points[2]), + "ptsBench_pro": str(points[3]), + "avgHeight": f"{avg_height} cm", + "timeout_left": str(timeout_left), + "timeout_str": str(timeout_str), + } + ) + + for k in data: + data[k] = str(data[k]) + + return data + + +def time_outs_func(data_pbp): + """ + Считает таймауты для обеих команд и формирует читабельные строки вида: + "2 Time-outs left in 2nd half" + + Возвращает: + (строка_для_команды1, остаток1, строка_для_команды2, остаток2) + """ + timeout1 = [] + timeout2 = [] + + for event in data_pbp: + if event.get("play") == 23: # 23 == таймаут + if event.get("startNum") == 1: + timeout1.append(event) + elif event.get("startNum") == 2: + timeout2.append(event) + + def timeout_status(timeout_list, last_event: dict): + period = last_event.get("period", 0) + sec = last_event.get("sec", 0) + + if period < 3: + timeout_max = 2 + count = sum(1 for t in timeout_list if t.get("period", 0) <= period) + quarter = "1st half" + elif period < 5: + count = sum(1 for t in timeout_list if 3 <= t.get("period", 0) <= period) + quarter = "2nd half" + if period == 4 and sec >= 4800 and count in (0, 1): + timeout_max = 2 + else: + timeout_max = 3 + else: + timeout_max = 1 + count = sum(1 for t in timeout_list if t.get("period", 0) == period) + quarter = f"OverTime {period - 4}" + + left = max(0, timeout_max - count) + word = "Time-outs" if left != 1 else "Time-out" + text = f"{left if left != 0 else 'No'} {word} left in {quarter}" + return text, left + + if not data_pbp: + return "", 0, "", 0 + + last_event = data_pbp[-1] + t1_str, t1_left = timeout_status(timeout1, last_event) + t2_str, t2_left = timeout_status(timeout2, last_event) + + return t1_str, t1_left, t2_str, t2_left + + +def add_data_for_teams(new_data): + """ + Считает командные агрегаты: + - средний возраст + - очки со старта vs со скамейки, + их проценты + - средний рост + + Возвращает кортеж: + (avg_age, [start_pts, start%, bench_pts, bench%], avg_height_cm) + """ + players = [item for item in new_data if item["startRole"] == "Player"] + + points_start = 0 + points_bench = 0 + total_age = 0 + total_height = 0 + player_count = len(players) + + for player in players: + # print(player) + stats = player["stats"] + if stats: + # print(stats) + if stats["isStart"] is True: + points_start += stats["points"] + elif stats["isStart"] is False: + points_bench += stats["points"] + + total_age += player["age"] + total_height += player["height"] + + total_points = points_start + points_bench + points_start_pro = ( + f"{round(points_start * 100 / total_points)}%" if total_points else "0%" + ) + points_bench_pro = ( + f"{round(points_bench * 100 / total_points)}%" if total_points else "0%" + ) + + avg_age = round(total_age / player_count, 1) if player_count else 0 + avg_height = round(total_height / player_count, 1) if player_count else 0 + + points = [points_start, points_start_pro, points_bench, points_bench_pro] + return avg_age, points, avg_height + + +stat_name_list = [ + ("points", "Очки", "points"), + ("pt-1", "Штрафные", "free throws"), + ("pt-1_pro", "штрафные, процент", "free throws pro"), + ("pt-2", "2-очковые", "2-points"), + ("pt-2_pro", "2-очковые, процент", "2-points pro"), + ("pt-3", "3-очковые", "3-points"), + ("pt-3_pro", "3-очковые, процент", "3-points pro"), + ("fg", "очки с игры", "field goals"), + ("fg_pro", "Очки с игры, процент", "field goals pro"), + ("assist", "Передачи", "assists"), + ("pass", "", ""), + ("defReb", "подборы в защите", ""), + ("offReb", "подборы в нападении", ""), + ("Reb", "Подборы", "rebounds"), + ("steal", "Перехваты", "steals"), + ("block", "Блокшоты", "blocks"), + ("blocked", "", ""), + ("turnover", "Потери", "turnovers"), + ("foul", "Фолы", "fouls"), + ("foulsOn", "", ""), + ("foulT", "", ""), + ("foulD", "", ""), + ("foulC", "", ""), + ("foulB", "", ""), + ("second", "секунды", "seconds"), + ("dunk", "данки", "dunks"), + ("fastBreak", "", "fast breaks"), + ("plusMinus", "+/-", "+/-"), + ("avgAge", "", "avg Age"), + ("ptsBench", "", "Bench PTS"), + ("ptsBench_pro", "", "Bench PTS, %"), + ("ptsStart", "", "Start PTS"), + ("ptsStart_pro", "", "Start PTS, %"), + ("avgHeight", "", "avg height"), + ("timeout_left", "", "timeout left"), + ("timeout_str", "", "timeout str"), +] + + +@app.get("/team_stats.json") +async def team_stats(): + teams = latest_data["game"]["data"]["result"]["teams"] + plays = latest_data["game"]["data"]["result"]["plays"] + + team_1 = next((t for t in teams if t["teamNumber"] == 1), None) + team_2 = next((t for t in teams if t["teamNumber"] == 2), None) + + timeout_str1, timeout_left1, timeout_str2, timeout_left2 = time_outs_func(plays) + + avg_age_1, points_1, avg_height_1 = add_data_for_teams(team_1["starts"]) + avg_age_2, points_2, avg_height_2 = add_data_for_teams(team_2["starts"]) + + total_1 = add_new_team_stat( + team_1["total"], + avg_age_1, + points_1, + avg_height_1, + timeout_str1, + timeout_left1, + ) + total_2 = add_new_team_stat( + team_2["total"], + avg_age_2, + points_2, + avg_height_2, + timeout_str2, + timeout_left2, + ) + + result_json = [] + for key in total_1: + val1 = total_1[key] + val2 = total_2[key] + + stat_rus = "" + stat_eng = "" + for metric_name, rus, eng in stat_name_list: + if metric_name == key: + stat_rus, stat_eng = rus, eng + break + + result_json.append( + { + "name": key, + "nameGFX_rus": stat_rus, + "nameGFX_eng": stat_eng, + "val1": val1, + "val2": val2, + } + ) + return result_json + + +if __name__ == "__main__": + uvicorn.run("get_data:app", host="0.0.0.0", port=8000, reload=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..99f3749 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi>=0.115.0 +uvicorn>=0.30.0 +requests>=2.31.0