From 0422e09400443de0636c02a7e236f8646a6baba4 Mon Sep 17 00:00:00 2001 From: Marcin Kowalicki Date: Thu, 22 Oct 2020 17:40:30 +0200 Subject: [PATCH] initial commit --- __init__.py | 69 ++ __pycache__/__init__.cpython-36.pyc | Bin 0 -> 2263 bytes __pycache__/auth.cpython-36.pyc | Bin 0 -> 7471 bytes __pycache__/cache.cpython-36.pyc | Bin 0 -> 6520 bytes __pycache__/classes.cpython-36.pyc | Bin 0 -> 42216 bytes __pycache__/core.cpython-36.pyc | Bin 0 -> 17655 bytes __pycache__/exceptions.cpython-36.pyc | Bin 0 -> 3201 bytes __pycache__/messages.cpython-36.pyc | Bin 0 -> 3450 bytes __pycache__/tools.cpython-36.pyc | Bin 0 -> 5144 bytes auth.py | 210 +++++ cache.py | 134 +++ classes.py | 1080 +++++++++++++++++++++++++ core.py | 483 +++++++++++ exceptions.py | 82 ++ messages.py | 82 ++ tools.py | 185 +++++ 16 files changed, 2325 insertions(+) create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-36.pyc create mode 100644 __pycache__/auth.cpython-36.pyc create mode 100644 __pycache__/cache.cpython-36.pyc create mode 100644 __pycache__/classes.cpython-36.pyc create mode 100644 __pycache__/core.cpython-36.pyc create mode 100644 __pycache__/exceptions.cpython-36.pyc create mode 100644 __pycache__/messages.cpython-36.pyc create mode 100644 __pycache__/tools.cpython-36.pyc create mode 100644 auth.py create mode 100644 cache.py create mode 100644 classes.py create mode 100644 core.py create mode 100644 exceptions.py create mode 100644 messages.py create mode 100644 tools.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f1e542c --- /dev/null +++ b/__init__.py @@ -0,0 +1,69 @@ +from librus_tricks import exceptions +from librus_tricks.auth import authorizer, load_json as __load_json +from librus_tricks.classes import * +from librus_tricks.core import SynergiaClient + +__name__ = 'librus_tricks' +__title__ = 'librus_tricks' +__author__ = 'Krystian Postek' +__version__ = '0.8.1' + + +def create_session(email, password, fetch_first=True, **kwargs): + """ + Używaj tego tylko kiedy hasło do Portal Librus jest takie samo jako do Synergii. + + :param email: str + :param password: str + :param fetch_first: bool or int + :rtype: librus_tricks.core.SynergiaClient + :return: obiekt lub listę obiektów z sesjami + """ + if fetch_first is True: + user = authorizer(email, password)[0] + session = SynergiaClient(user, **kwargs) + elif fetch_first is False: + users = authorizer(email, password) + sessions = [SynergiaClient(user, **kwargs) for user in users] + return sessions + else: + user = authorizer(email, password)[fetch_first] + session = SynergiaClient(user, **kwargs) + + return session + + +def use_json(file=None, **kwargs): + if file is None: + from glob import glob + jsons = glob('*.json') + + if jsons.__len__() == 0: + raise FileNotFoundError('Nie znaleziono zapisanych sesji') + if jsons.__len__() > 1: + raise FileExistsError('Zaleziono za dużo zapisanych sesji') + + user = __load_json(open(jsons[0], 'r')) + else: + user = __load_json(file) + session = SynergiaClient(user, **kwargs) + session.get('Me') + return session + + +def minified_login(email, password, **kwargs): + import logging + import warnings + + warnings.warn(exceptions.SecurityWarning('Using minified_login in production environment is REAL SECURITY ISSUE!')) + try: + logging.debug('Trying to use json file to create session') + session = use_json(**kwargs) + logging.debug('Created session using json file') + except Exception: + logging.debug('Switching to regular http auth') + session = create_session(email, password, **kwargs) + logging.debug('Created session using http auth') + + session.user.dump_credentials() + return session diff --git a/__pycache__/__init__.cpython-36.pyc b/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26a2ca3ae3269bb73544aa7370ae05867e3eb36d GIT binary patch literal 2263 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 HikALAEf~mz literal 0 HcmV?d00001 diff --git a/__pycache__/cache.cpython-36.pyc b/__pycache__/cache.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05e5358e0bd7670f77df9de37eea6d574506de8a GIT binary patch literal 6520 zcmb_gOLN=S6~+ZX5a3HMKNZKJ>aL+rY}^4pxq692@Qm))ijGx~kTzytb#c>OTU9 z)?$gxJF$rUP_(S1-jxC?PxKG`zDTUSSo-~g#QbXSK|8VT41!VEPwczc@(>Rs<$L{D z90-Y1R=MZ~9odQfZ-uwti3G0IA`wOYpdWS))t<^;jQicM2ZNwXFQ5K&Om@*Cj!tMU zZwLOq97Wf_%4U?hW>Pq8nET0dYlRlQT1*Tk{QFgKpwSBW+ZeYna5)tYh{JEChDFJ-Koq z;_TF>6i-IJ6iq)I1|k%FlE$D9L2a}L@xAFq!M~JpAX|oP;5@lP9h5nxF{O4L%cL?t znOLv{?|eg75rbVcz8)_Z!D)Eq8JyD6bNFU5kSVnJA%uJhwE2cEiJX2kz82a?-9U6? z1}-|0n<+Y$qFVq~FC4LfH%_sgpNqM$=gr|=?G8r$Q)T@+bhZ2m=W^iA<-p}$a;oJh zZE2Z`6|f!YE3+96`VoA`C=$~1`P_a|**T=;PewwHFXj+z%n{7)zqt`KNfXtXr${Z+ zd3v*akGciFOxY;GwpWUJW`8wOI zt49!7yN9CBKVA;Au&3Q>B(~@EJE8Erq~dwufR6&2*F5N3CrIx|@}*-|PJPfR!A#MAjX)3@in_BGY~`X@KI)$Ou9KXIV(Bk(@7gH7i z(6QpHSQ^;ZZhke2;wA?pQboFzQo20wKoO)EO3%BVd-3}G#napEmLwE)aouA=nF*f))ejeirzrZhI zT;-emJ&bGo5`Q1#I=>9AHj~=z045oZm1@0QkDdg6EOxfHw|7Dj4&-=emdFq_CA2$4 z(I3&p+7Y9i)n_r|#tY*+J=TwOZoXjO8AnFXNasvD0>wv~XC7(2QqSs@kMv_k$vm#v zDrqaV-8F#d9(oa(Zu}T2Isbi2cM+cZePoATM;=73Go&;+=tL0`ICJ0c^Ohq?L1mRX zN+A;mPdxGuMiSvR%f%Cua^A!aMchH+-*GEh%HIisq#BL;UbZm{+ioST1w2G=*GlJL z_x^NSP9Ie21x54we(ZTW01%Nw(Ac_OVKrv62AiDCGk9ydce$Rl@$6JjICBkQiqbKP zC2&^<6jPK*S6(39b#1k>vXz5Xj>%>L!0oT^eM9D^rau}Aq_N!B23bcsGZl%z3| zoI+9>Jnai9Nqyudb!EEhK$fu=3_AIgXy#j!dI76aJL0a*OuYflHduwp&v9WnFL!b0 zEqLiDo>joTs+!ff1@4t6mkQi_po*3IK%DZgmR2t_7E1DRKhmoa+kT0m-KsCJOp-sE zGb*h%h*OD4OiX#SMtg`y@-ym|c%Wou0n2@CjK~J2`~i1JBMT`j9jO?(g$e9Y>oN5D z5j)ahuSRV4NZ;5ClPpx-w~kf8t~@XdMAb%L;9;pC61rAd{v1PMAWcWDdQDxgwqeu$ zI3`=*RxXwkhv-M+Oc5xE&ACOKijAHLhiw$p0DL2tJyio0=@lfUpEqy_!YC0ee@z{k zr6diIH>vv#x+zKXN0*cvDg)!#Jrn;0eR8hA{pH2bO!}0<&FPs+U&$U+5&p@IQ{-E4 zJ9+6U=!(`-O6x5mPSN^Xn(d;Sene1vmr+Q0MGq|^_GqOA>XuhiGwMFUBPUarnQTE; zx^loWFQFpp#6poIpq5HVr<8*#FMdMk36jPxu?76|f=iKyJv16<$~GyBU&IgDFw(eX z=$W#pscI?UWZqJ-`!>xK>XSc=s0@FEjTzw0Mf)JYsS__GpoCf>ro6t0cxDN#4V_jx zKj$c6g6mh`kru3#s);dLDtNXKm(x!mSy}h4P!x||1BEJ{qf?-yOo^65Ay>V~71-7S zM+inzQlD5(;0aY-f6D>6STObc$C(0-nLRY|ET<5LK-NPD`yR>CY-N70{a}(uZ%yRo{IlX*(%yD!O$8{of|93`)<9`7LCTe8VJe%YNU2gv`QfBTMYb?7r$(_3FKm9UcC#7d%(SV=aLNyby%)JnRMUdc2v$;3kmD{W<-Pgt3)R3nR6*2*E4 zLoA0_-YOtguyb4KMjr8^RYJUE=MgU;K4p~=FU#{H;?q_I@ruMth|gG6#H$jYLVS<4 z7xBFkFC)Isnniq8;?s!lw+omh#!)874bJ%hY>$4@jZyY z(YhA#YbCxH@$0M_;x&oyL;QN{2E=cW_$=Z#T1OB+BJur*-(=m4_{|bOfcV!~w;+Ct z#IHeo-ntd>TO~e+_?xUZBmQQIA4L2%>nP$!C4LC;+pRkgzXQ;G17dHn-ip{;5j%|7 z+pIegyHoPM5%ITMcOib4#IHsCZtEVz?~(X*h<~kh4Dn+UuOa?*)^Ws-OZ>k|MiAK^&G*VWwk+xD+`uRj7V`Z!? zet}R{9=|!OU={J3w@TI&ehXF^Rho9^norm({ek`1!jjz{I8UCw|It>jwP>rg`|#mX z4S$c?y_R+9C0o_{3mtoT@TJeLzq!UAW!J3!YR6Xn+OY2oTD_M)Qd=9W_tmi9vyaNV z?X*{6{<6-ea5-}b28pf2W}=zgOd?EerVysBB&PbJhe8u`k-w+~iUZ(r@K-nOvT=~}nF z{cXd$&cAJG<96Mm+jNV(nAg7B+ufFf?;Ty;KwX-hUT4s3P7!W71c_`RnM)4Gw&SS( zMw^_7fBYr*1@wDy17N4m0pLrC&E#=(0)U=e`w=`a@M^SHJI>k?`nT1yYpZIw;S4W5 zXD{|W6j(dDt&5z2spi`)%hBuDu zs`M?mos^tl!{y1@q%A4pwHT}*GLmy!YmaG#H* zNWW{}h)kxckF!~-RaSwz9!^Rgq~LX#{dQCkbN|d6CVPFpNG8^6t=P?`TWU5}`qo;P_vL2unYC8edy;Q9 zt$w@NR5SR#+Q(o&gKHRYya?5ZoK+E+iVTsLQ$|ooXHtb?pIC}u)dkF+;x%iXp1kfp9~EWP}_ZSeBZLiNWeS%iv~XyikS{Xu`-8%=NNtsX@& zXoA&W=kPkLwOj4;_PnWLeLgEpN;4+nx0}}^m*ZBNOAhMmx6|iiWOi=BPZhQoA{e0+ z64w2=98yAJDhDY7yzGrW0)-JVkRgae{3AZ`l0ksu=O%E8JPa6>+Dda65Z3<}cx2k~ zd7#%rs6r_-_o(lvot$9tq@x9!Yt1j-MEV2Um>CHQo*Z6N(> zg-F9k>;3g@1CECEf|o3K$pJ+S9o7pi-=k>j-~9sk@2GnpIQO zZ1;PX(-vtE-{2T$`sQH}jjxt#mT6nLh)`#wsnR)Q1OID~lmN_X70N7t>ow`9dCCBY7_Q zk5X2CGuugiNAhApzEEIJ?;9B@^?L?|rQ&8`D|s=qS=h`ROk6AicZyaKxKrFr19wWB zCE(7~+A;DT3jO@^#fDDJSthBpYP|sJ+i_zDlH_1*)o_E_D%2`SsvR3*Es(U|YlYYh z)Gkwx*GK{JJ`=}v9uAmHyYuo#7JHp|R^yN2fQ*Wfcw{8653hehB%@Amv38Tg|L0VH z1%yTIPV*Uc15jjGq+(e+*9XU|)2h^i=}r<)Zv*C;I-LEOz+_QKkt%Hpv)$UOV_Q1V&i zaq?;PMm{OG)&~8i$Fk?AG?5A0%dj(r)Z9vxrMo%%!YWj0brZhOsI0U$7HsobVVAmq z8XjxeXM{`-;DLq9VUEO9AyrN0Qk7%@oD;G}a&PKDvWnjlE|BmWVtJ{SI@_dP{!w%? z)JiyAI81?h3%vP*)r(Mvt;}*>l{TRYKT~=N_94(lcJ14c(ct9c7y=iloe?A}Em#JI zWUT8OR)1mR<&WxNYdehy|6FZtbo(vqnE9CDYEQ}Q18>v|J=YRtcno!mPk5uV}9l?Ck&0sinVA05XrMYP&jIzLaPv3$rvu|&Hz37YG6jwPv8YD+a`xcVkklS`>(D3$}5hnT*Uc_Hy)isO(a8dQCF z=rPUSz`%O_0dybQqS_+)0i30d=pj2N8kbx~k5I5T&oVV2os=B#m z(|)Ged=WXc4JKK}zcKW#^G4cdpi4CcZ9ogRl4TE1lj=UCdKFM7a352A4;s@y#Umd( z<5>;#EaZz6Z9q#_7J$~-J4jI&&|-i6EOg|1S+W4=ow%D+pGvdiG~4|Z@ZAn1>>psg zL>K{3V4AuymU5!+_+!K#jpBz`_8S-+WgyO#ixQZsle{BZhaeirU>sK1R53@2@;F;Ar;>-SFINg_`IjxEW$Nq^0jb9$9A%wmGeKYlcaU+$};O9f;Zm!5lZN)Y&jTfCXs%x-0UPJbFt1vBPz zfncC+Wh$j(^(F=c+V*M_pMDINrmWQPz-X06#&3=|d0a#*5es5CJvn}w9A9{Rrmo(O z%<3)%cOw8q1$M&S-_6vAnHoDr2}=JYiG~p3sn=g%0HE~1T&wv$^p3=1XIHK;fH{iAHZWrU2 zk-_}V67}^#`R5!%5R}e2o^07DT<4^YZuB#HR3;Q}glM;sRa| z(@vOMyrB68cm+pQPQnZ_OVsyJX;>*SFS(SHiJH7g&qmUz@fDX!R!-i@<5}TgVlc%r zEV8uH+Ml9$BTk;>^pLMT!)6k04n=9CsXm}9EJ1k=OK`uupi6ilXkbKCsF zE;N2^eAlK2SY)6A`lhvD7RgiMM(hwWMKY|>Eb}!j(@r;6Ut3wQm76=Wv9i$bHfB!R zP=(I4&-eS?_t`BqEZp?I+iqI9?Ix=+{XowO9=O>7#Cp{zN#CF4y}BMCtk0K~@HrtS zVb$4Ix6^VIvC=JoN>^o45nOa&H%bdEugQD2VD~J!Q?%b}>niDHH>~|j$S)+7C{$C2 zl5_ZnvHoxjx*8@Aj-Jz6YrL#k#wW>}mN0aRw3(`__p(|kT~8WG#&AVu`z>5P*FG3m z4}oK(Xeyeg_}UzT7{;A`kweOMhMf*E?5FVRm|9W%_#J+pJ(3s;9nG(|bICyVDflxU z!rlZZnqR&L-yOps&*0G)ad`|flPd2?C97%0^mt}<5>I?)6&7H$PlCs=vS3)8LKcPv zfkvj0X$3bzODV8szqg|Xu;~lt#`B_cD03VyBKHw4;z=%Y4=#jsu|O^5E$C0+Oyod0 zaGAnZ8fke+1J{wXVuAYqa1<+ ztdC)B%Oh_?K6SU?B{^yV*%eh+p#Nj|G1CbA5HD%x>#A<0k^x>Db<3}FjJow&jM`Tq z_?)RyNNM$}9u<@%x)!?TJhF}?se?So@u5qB^>a>C|^cn%?k8%ay zt6`2gY)B+E+#kh`qnbT{Kwh^|w-zeha3>FJujhDsWJ0-)Cuk+r5_@NQ)MycBUl|#C zi9=T$e>=!D&3aHmgOR0@f{}$Ia4MTp`-r#Ki?0NgXXWxI*e3*f7zUy(v zc8*#x8nbPd#)e?-rP=Zdxjus4<`^ zxN&nc7A&JMKzH23ccS*~TM1&yMv%BtrL5Lp%9sbP&=_)U?3WF|>uyeZ!f{I;DtBzh zE%<1Cm4&i*uxUirL^@>E3Dcd&dm~a1L3#?8b2EaCGRVPLb25oRYPqDqPWg|s5Kl`u z4=WE50+=pi9j!cW;bD72X5#>N>9xQND;8M3aFR8YQ7x4MhDV&2BK3^p(&Q6T(XYbt zLzEm-KPo$?4Fo5kf~Pg6l)@Xz;TNZN%*}<4M6a5lL-nMV02Q@Q@bG;pFoU^tIj{Jn zjm!KQVXup%I1V0If?%|s#f7yfT!msdIk`7EIYd5qMNUKLqdz1?yb^>!@rk6k=Xphz zg||DE#&DCt{T(ANO7Wy)CEz&rE~{5 zZr;1Q%Ixa>rUB{DJiM?0UDLkD3{Q6OqRXCyKKUU0}Ce(K_Uo7TMA! z5!DJ+;<^YLdL4B%$QwbO*McKXz%sI_^QLZL|Hg19`KmSAEDUjz?XSc#M568J9O8(K z`wh%;CH_ba@KIK5FK)meOSCbOLsHHyXB3~ub4l{gI4&tFah1FhaxOf_4R_@FJx|UB zH!OlXa`9E55;KLK!*7H3g^8YdtHl|>&P{tT*K(Cr+Q)Z);IGrA(l>JR8KPy=3Z!7-bC5^SYTF2 z2jZq*ZvI&~6T#(Wg#U|zF0;bBVf`yaLSLAi#eA9;C#kQ)AZ~=oNWYs=pzc;84;;lY z92|cIKUcw~chQh~NPLXc@b7>jKW8ctnox*VV%HqGMX+9xBa3GJEmtnyQ{*Fzc;bMt zwX@=x?b~)xyhj2_^{xDFJfj`4FOKk78!u`*0?V9abNog;@3KxFqpb#PR*d?59?xih z9Z$wG+Keo`5k`vzHJF;@B54SqnQbgK1FK_{#b)fXr1(PWG+4JUq*ib*i+8|)XY4A( z-J5V7u=j8ov|;I70!@~_S1*2U?9+>$8?$<`b7Q|=)BvR zw*2GpOFyDCF6R#sj52PlA*S&{nD)iYD9Z*b4%oQNhZ;`}&Bism)@&SeqsVVqwy<$+ zB9$j#?qK4=sHePNFmZ`bA53gPhHldMF||~-Dv~+_c3uVBCyOszdn9eImj)J(w0)8` z>!r;Yw!gm~>2ad|2?Bs@(C=~GY^-Faqf%ygTSTZa3@|WX$SClrJ-{M?K`X-g_<6Ct z+<+U}Ua$g+J}y`TfiOiQ4`!U%KJ6t3>qOSAh;t)+NIQcys3h(l#Qg!FsRx8NZcA!% z!X67+Xd00(oa^*jT`&6~WDmu|dEu`{;lGh#p%OZcEWf@gk#iC`w>o0`5pTUukhHYs zz_mR5H@s_Curjd3^5|HRD^S9X8N{8Afcv|Bo&&F_??6M;Hz9}_(A76HJvyE@rcXPk zuy()$w0aI%8l}^Y8Rho}_m2nQZRAfo4-&eKz1~ge;dnWq(DuxSYJhU)t7C-#^$|Q* z9RJm~F!*~6K8nCCz~5AmEOz*(bQo`Q63Ad^jxi{GC*eozr21DW1X2KbrT$k*dlG>$ zMoSNv&NE{Pu^pca>@lFh1XmO+C7o;OMJ-`k!i>6sE^!uZkLmi^2T}3w#H;G>Gx!*T zZ)5Q72r&2&%iQG32o}<_8i;CRU0nAwY=^eZl7~MP6Tld zIcP%~{0R~=JQ%6XXP2-lQ)2lS>0ed|c~=gv)yrLcD)6S&YU>@Wb%B$Zj9bHe4N{(M z)#A|fZd3!)G161w7$_(p+Ws6K32un1)j7IajZv_vfqb3-VUHw|flI01aoPwJ0fgze z9A06^p=n|iW(CHSLyhPQp>m?;av+F5nfQKWjFiTT3E1Ow&?ZZ~vc$t|KVwP{&D7{t zzMg{C&y6p3*8)yHswwWsI@GmX@u>!vE~Y+}_uT*7M>fnXmz zbTK&)|8Y!npjcD_Q>ho;bQ!I zmgW}x5B)OB5?+Ma&r+%y^6A#7>1Q+$!V3vDR7m0|ZYY9KpQkpjCxNKBQ&*>sPLc7G zRjlFw{KRi@Z(ZGpw-#~X@2}qJ-TcivFBA4&EOWxU$(^`NtngxGKe=+^GG#+Ad9R@fS^wmG{5i*e)28;1&n)0VP*meBlFE2q3js)b+T)-nsE@XIJPW8En50)0qwKICr z$@e3(BI8icAON^Ij;a$3qHvbgDu2oviFT004jm(PnzX3sW_&QFPMQ7@T_+CxOX+2A zvBYlc#>G#I*ns9QCRdMHA`6cU$i`GJ`x;gci=O`a3e_837j>oTB}S^}ErxqF(CY#; ze>=H)%(5%!C11np0W?W~V*>7RzYdTpEwu9oV>Jaa5_}giTgsMm$u$Gab7@Xr|ha|WMcFk#M%n)SyCVj>%Cy0vn?kkP(_+Is<(-AZ|X zDv2c{8U0Ud}r9O=})W6^tceg-n66OC8K9^%H zsL6ox^V-OWg<*Ws#%2BteRG<~H#UXWJ;*%H0nqo@g`1e8k?r$f04(6%-vtN`j%Mx# zyE7V-qM7&hb_1FgV_*eB!Zq$*UkVPu70(O4AGICj1;!eiFxm;LgO!9kB&U8YgwMi<6$z;kqtb{7Tj|_6FgU4pYKYIyQW0=tcy#&#fB!78scd;L*!%@Ztz=pjCByJ2K+Ag0lr zO-4pz4fcbO=^9Mfm?hakhX%|{aKBLs9$hZsBUexSz10wIdT7>81+|OgUwC7j&0rYp zU3AP-RDJ9|t5>1~y6G;g3%8p}W`~FT4~8naOB2I{T6$B1K`n`h5ZJ}=LkFcXJdoM$ zxdi~)$9Gn5X1hOB?>Kzbv>vIs8E?6^*B@`UxDeWG$l;B+!)uQFRSZDE z{WyjN_$M7#)L08D`>DQKcEl-BCP}Wcoc%t7#vvE4NcCgZ3gMlxqtq?i?WsQ9 z3w#64#E#kl`)9TfnOf$8K`L5Hs!|cwM0Ma4F6kgrxHcOlMZ20n*yl`Hp|!=4vV77` zsh`64ck5ru_6T#CByoQpnVw)pJc&E!(Q-lYNjwd&;>lPV-Zx3&*7COJUs{I0l)r?C z{-v_NIb`uE+{@nPxDR}%X5F&iy@XLbWgkFaj8b--bS;vx*0OWvIW%o6i+T6F-XTHq zjJQl$3GBSLWKkn8_U6o+vQGluEXkW%?xr&pbAONIo(kpWlY+_3MF-`jX<3kvu`pv# zmn+KQNDJJpn5O65XIQW0+;=7w{j>RIRcgvveCDdk>^x@in|pdWYcGGrcCfELPM)U| zXb)D5!RWN^XVA}X;Vh=c9>lhDnD6!dxpGW9tL6Ph@sw=D>pR?Pd(Z%{>cxpI++jv? z@8lU;H;-Kqr4BMxOCh0A;-nmAy;^Dj1p@btf~3IS5-?S5mm;o;+m?H+Sr9-h0Ric? zHlE#Mup6ekOtLtFFDEeTFbgXOeY&u*S!zy)vKDI=ZoY?*7A$RIxdAH{%54;Ueym== z=fD#l!%xJyfy_OPOUBeN3csdX<9yVxoYc_lO(GSJVZM1(NBA1eaK6ap3tX6A!tRlX ziRTGtb7gm&TzLN0I0y` znBF3S&(YU&#`Fh5%{Q~t8hbo%4D=jFs-3x_u|L&_KN=gTImiV0^9#td#JW^*1NrlC zN-V*!4bPNwicjOmtva5J9k*WP7GLToSzHDI|M<&GDEZB}2~#o|GY}xw>?_ES)`Ddt z4fF1pwdHwi9N1Kg3%lWCKWq?2FDy5dWfXH$_Yhs_y`R`1=PHxc&l>DX1=IRLm19VYoa_`X zSwpOa0Ez&z?OeJ(+-Gbp<}2Rtp42cMZ%w&m>XHmpE%OzCZ#Ps$O?wRWlB#|&RF*L~ zh0F3O>k_~7+ELca_}mv*_6TJ$y^YKK5zPE^4kL&gqhzjuzC3Bytnl)zu@5TSz8_mH z(v0ZD4@F1t)p0`+H(P*~koJe5B=U-Z1N3m3_hLXY_Ke+7$*-~L8GM5V_Tf!FeaF?? zkP8U(AWI!FU`-GQejeF>)$d?sqbSbL8Ds9R;IaB;1o4c>oyHVc zr*Ad8c5iWT{>whb!ewb3WgLyXfzQ4lU!Pp1VE4V@t>X=3UULHz*)HaB{@7GJ7fkz) z!f)X1cS~l%KmHo;;Nyt+`eb0jr(GOFi|iRkfoV?fx+?l5*ZzvOnG-MJe0_M(3_K%6 z*M{lZZM9*lcFS-I2Yg`Xa?w5Y+~nOT8BY4_B=xTtNS{#|*EmS|F~0Z2GX6hJ(i zr-;T6Bcky9DocEjff(UM6Pm!`egQdugJpZeNVS}SvXfU#k8nC8pz+&dIbC@tCOR2# z3K(#v17{b#t))NS#4|e%x32-SgOj)nO9f#_FuDICz8hL{@Jop5QK}dkr2wPQ0zmEd zFP$y@{BAtm4e$7Kf?Pm~;fuT@!1pL547*o_$;~J)ZRL#gnn5SDHk6LUKGuU@9Ov zT3QD@`RD=IXM@dhDAMp^LmZYEOLAgr5Gf+F1xIu&9oG8*Q?5V?PYrBlg}|^$Y7Bva zRR$6ufZ)-VjNrxAv#m~-HQNnIWl&+GH|-5|XtbRnf~rQIV2_nXC6cmnner*{=LmkT zf=RW=77;dK|FLKdJ-R|2e}h#m>;x|-pyaQ!ES5^vq}6puy-aQ6GJnXCIDyMKkHAOB zU^Sivd6%2Q(kWgfU(XFv?(;~O<7ve;Dr+WQi%1t|NTiqagblU>k;xhymbMS5I~#|( z&vG*m)`x%mH3pk6BEoYF=n5$gO*qbs#~JAz;53>NNxf7~(WFf(k3@na6rSUtBX6&# z0`6ow2I)y2OT@*H;8#;#3{xcBG##6p5c!LP5|a)E69D{&VzlbOf1&}$E zG{z;j3?I<8)CHAsb6m)^w%``w7RrNUY)g*X3kl>lzej1p4Ypbeu$vu=IToJsL+=op zIAF>qt5h`xQw&DD<4LYnAb~?+D}kw$1Nc|MsXL|Q5!o|kZ)$Hk7-^%08O9LtuO=rv zh0FO60t`|~p1^}$xo$>B(cX?zM`XhU=u`taU7>d;G$}vVBcWV^U?vyW$Kr$%tRBI< zAZGvRbe|i3ZAh+;J2B2k)&5~TCING0KA7C1ftYAfjq+n^(LCjuad)6Cp^kW&(ftVg zl?OE@L}A;Q8R?803FitjCUSZ=8VS?Gn~f@Nh8K1vpd0DSh^j#mUVzMg17L8*5*Hm3 zL~52x5A_P)IvVNYS0jJwe5c6+!_20k(l???*gDZaPVNZcZXUK zX#Y_vFq9eT1H)vl#b*K>GP1C2d+!~{0n4Z!0z~))OZ@kEN67@|e_&l>t)lM1^J^15~St)8Q8F_n2CzhIfpi-LWr6T49v%qo{|L=Q-ATiZ>^8MFGbz6>qRS zd1ln^xj^lXz^tZzn=Ry+HO%Q1x*?4_tz148AdGLnsMt@Mmc|N)9+!$S4I30AvyiGo zqyWFh3@fV!kh2c)kQXnRr$Z$h@Anbf1b^h|DfKaXcK#=}QWX5(90bQ2U>Mq7OnsOr|*>W?m= zkwcbLUj7RHusr2^kbvU^#TgTOkTq!%gT0^l;TB=D(M}(VsN&`shvec9d;f(W3t%T; zqe$^Vf{pAw0b!zof7~lKaFh$TIS?XPGyd_{P{x}PnWX7@w2{ScR?JbJ(}R!r!#$p# zsZ(392kW-os|~X?Y;oz$jO0R&57L@HK!NJZ4E_+o7!)PqoMRi=Nmzz%HnJaSnI;Vi z5OHR7LB9Zc0p|TaKKcQEo@@@7S5!=C<1&9#H~8la5d;dw=&?AVbC>gS)&vB5Szl4W z)+^Xb8aoSO?vGorh$;abYn1FMVZFktQoY<1t&(Df{u>lHi{&bNHZuOL1YkW(H7+PtprXo=2_YLRO8Gh z=g*{{L{%a@)PF<*!+V5td-45I|Fc-U91AVbSk=@AY8Jf`n z>5uXL#|f7zZW5^sb`-8+1DFaRb0~BwY5M3V{8<1W<`w;I z{vy{o4~UScI9|@@-K+?wZW_b41|#7nLW(}|oI{Am+4S_Ca6t7xkPI(hF&yof4e#Man;5fspz`)J{)Ev{ zJCA2?BI5lZYfq%YNFB?cM{+7PGaD^82a|)Md>jJ9jO;ncau~= z1mpxl1~s19-bsK2j>6V~d-kK`O*8K^N72Joum51{y#Xb-$r5JCYEVv0gEaTc2&p%+ zeG0qq)B&#Jz+X2j9X8rk6lFgZbX5j;u5tZvGFXZhbd@JS19JZzFV2rYFjR}tmyt=Y zT_Ljyne887lvJjOD_~O zD}_9g7I_3zN^2O%?=*h26UQfb_imjk?0gRsY7EHs(JS}xK9CA2SbvTnBYuh#hq1lH z)Yga{a3AoR;iyOP;$=!__&O2I3~WSqUikd$?DhN;Y=IDCdU%7cJ;&8Q#{bE%jX6|v zlx={(c5=p?Ks;9E7sUN(Q@xOBE3SImT%rg%Q^9v3qWOgdBRX}!kEOvS?vU}~7VvXO ztQaUEv2y)@nv*zY)%`e6E_h}RXOvTGhAZkL_#Tul5f3O2U^0lZNTZ?;Flg#?;~i}r zPJ>nG-2@(HR&4`@zrJ<@aI`HTDm=J*vE;6O9tX-dISBM%+TQpgK|#z+r1paC*Dz)d z&4R-?kZk`1s{H48U+`{co3V>d^Z~x}mkcIwkUzuo&$5aXcOd3q7M@!TgKzLj8<+Vr ze1i-!o^N0-CH+Yp5N~B=%itUqxaFV{<*+R~wik}smOVI3qY`W+9Dz!_7DKsa8^0Li zh&;fKY`_SaClu`_?0FQN)hC`uf+^g4{bb+TkkuYIJ^~UG-Gt!bgF9^Qn_~)&B)%S% zKz{_x=sG00nFYLxy`cK5owimUrq5xm$pET>$m5M7KQnsTC86!f6(a{i#IvwpHfV+6 ziThPx=FymVA^g@qt(#B zs|zZ-d5@W1W#`e~zH+rS0|o3zf@7#pD2+X+2#e8`sS~L0uaNzh{GJ5$VRHyM%7*D} zT;|Wv9*gp6&q|IODn`%m1=ZzYhQJQAFj2(OAGS)cDlkQa&$}{<((3lQvqt?aY=xkd z7aZ`5gK`%yJ=b?|;xCL3&TzTk?dgTz@pSur_}V18HD~xj2IRCTZEixl+I5DMD-?@# z?i1l3hf_4~dF$5jykye6)oDlzuS`qH$~1g8v_nL>u3kb78|7160EW3t(tX@Ej3j-{ z9`aAMoF`!3ZWPaON>W}h3RxrnfaV=~v{=Vm5kEpQ4E3)WkalFBZ!8JoSkMubziA zQNRv(2($f3h#1WR2`SwhbL&`cEXNbq`-4u~zS^GWe`@NR4pyE?eJ2TB1i2QvsLKF{ zWrD(A=wC6D5&RI>_hZxE0rmpbI$KIGL4Acl9WQwCS_?!m!AV_YZ4@pPpSE$CKf~TASB__I5G?^^@NP&$r3&?ycoBvdE!9l)H3EstL^5Sc|J z43XD#p=SmPn=fJ*=Hyj+62ZNGT28^#C=QH<($b`~ZJx6MrQHvxh3o)VA}tsI6R)rn zsZJu(^mwAW7a4t`+NS4E!F_M+c5%jiuN+LZ!CAn#`os_Nqi8^vet$wC6^P5b z!P@B%sLeD{xF!MKin}C6icmG3MmZe4Ob<{X@>SB^tEMfXZ6m_<^PTHF&(AFJ=d1Ow z%itqr7ED%)p@iYtpXCV{6NjycD+VtK0wnG>6G8Rks;5SepP zZ$Q>42~JHxA>ZSg_1X}ix}Bl$vUrr*M0LWPg(wBS>hanEcHq|h){_~tn&*Pc=3pVn z)6*XJ1*GcT5Bx)7C{LETOBmhi#5HaTyB284kSW35VdZ)wdsbAsZ$?!DlY1j~+BvH_ ztBqN4LIJnNX77utwW2eB2AKodPcJU{ai%RJh=_s|@d7DOZ1#;jeAx#0xNL*lsEQaw z;{x{bXA#;n?pnUMkHK{e9%Rr#up9EahL}OKh#nMZs3_x-lUdE$tWYNzYTKzYIoz#F zl0ef|W;RZMxI;nmh#|0Su<2R2_MSYsp+u)K6^dQu zar&*M1?ga(AXL54Ni(pZz~c8+4Avx|2}1GRrdpw`X~Na=O_BIU0d>MLpa=2A(M5YA z7K=nIWPCrfU@+dH>wO*{KER4_76@E;nvO7@P|Rob`6HSK@RN+9YXRG&A=c*@=wx^n_USF+N%aunfmFns00i@Kb Id#k1Y2YD0FB>(^b literal 0 HcmV?d00001 diff --git a/__pycache__/core.cpython-36.pyc b/__pycache__/core.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9aa435ffa6ae7a921c736a8870ede9bc097fb467 GIT binary patch literal 17655 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 literal 0 HcmV?d00001 diff --git a/__pycache__/exceptions.cpython-36.pyc b/__pycache__/exceptions.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91ade1f0b19e0d3e282878f8db87aebec8dcc0f4 GIT binary patch literal 3201 zcmb7`U2hXd6o%J!5`V;>ah$}26ar1bt?%3Zs8M|JGP_oG*^?S;6xY zTyZbuI4>OO$kgAsl=D~SG6hb{3^=1w;52weu7X!p8k_;I$t*alc?F!4d2n83(7p;T z$aV0#ZeIfzWeHr;?OAYHR=^eQ%Ym!12CnJ$Ja|La!FBB~fH!3W+|cdo;4Qfg-q!zL z1n+g?`f`pzmd1Vw=`G5x8*)~Uvmw7N8SbBRU3$>4*pgi zfDbfpg1?jZ!1pva!1rYf+)`z{za_svcUnJqM!Fqjd6w@Y9+Y#LlFnsXrtn^R+IZ<- z(Kzmr@4b!NA3YT%ei)p!GSi&v24bXKcUo}WQ7FelelEK1yRjI~T0S|1v7Oz%o*X5k z=qU1|!>h4Bl!p(G|NLoi{Kw>Is11jSR?NwrlUf+#AEOCzNBN4=xaCrsts`lgk(`W^ zff)_)KT?KoJp2CZ_Uvhmf6L;=;VkUT*7lpG{enakDO$DzISBJc2birpP@Q$)W!U$F zHc?0zLz}2sSTdx{*3#YcNua3ji<54r9mpv3gXAnso`>T=TKWs{)eJwgwg2`k=x#`f z7`4{0O_+%;LEL*=s}2eAe2Y=C4mJ`uL?!MZ_l&;*$aK_DsAz5^q(+dk1j|CoZeR zRgSpi5Z3_W1xdU@i03Tv6q;t|V?#W+#Hl8Zo7n$<$Fgbe(d_W~i#{3N$7|IUyYIgv CtOdCM literal 0 HcmV?d00001 diff --git a/__pycache__/messages.cpython-36.pyc b/__pycache__/messages.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1df52ba5fdab064aeeace2bdc9be1904f40458db GIT binary patch literal 3450 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 K(nbHQKmR}5c3r^$ literal 0 HcmV?d00001 diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..a6d4696 --- /dev/null +++ b/auth.py @@ -0,0 +1,210 @@ +import logging +from datetime import datetime, timedelta + +import requests +from bs4 import BeautifulSoup + +from .exceptions import * + +# Some globals +REDIRURL = 'http://localhost/bar' +LOGINURL = 'https://portal.librus.pl/rodzina/login/action' +OAUTHURL = 'https://portal.librus.pl/oauth2/access_token' +SYNERGIAAUTHURL = 'https://portal.librus.pl/api/v2/SynergiaAccounts' +FRESHURL = 'https://portal.librus.pl/api/v2/SynergiaAccounts/fresh/{login}' +CLIENTID = '6XPsKf10LPz1nxgHQLcvZ1KM48DYzlBAhxipaXY8' +LIBRUSLOGINURL = f'https://portal.librus.pl/oauth2/authorize?client_id={CLIENTID}&redirect_uri={REDIRURL}&response_type=code' +# User agents +XIAOMI_USERAGENT = 'Mozilla/5.0 (Linux; Android 9; Mi A1 Build/PQ3B.190801.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/79.0.3921.2 Mobile Safari/537.36LibrusMobileApp' +IPHONE_USERAGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/77.0.3865.103 Mobile/15E148 Safari/605.1LibrusMobileApp' +GOOGLEBOT_USERAGENT = 'Googlebot/2.1 (+http://www.google.com/bot.html)LibrusMobileApp' + +class SynergiaUser: + """ + Obiekt zawierający dane do tworzenia sesji + """ + + def __init__(self, user_dict, root_token, revalidation_token, exp_in): + self.token = user_dict['accessToken'] + self.refresh_token = revalidation_token + self.root_token = root_token + self.name, self.last_name = user_dict['studentName'].split(' ', maxsplit=1) + self.login = user_dict['login'] + self.uid = user_dict['id'] + self.expires_in = datetime.now() + timedelta(seconds=exp_in) + + def __repr__(self): + return f'' + + def __str__(self): + return f'{self.name} {self.last_name}' + + def revalidate_root(self): + """ + Aktualizuje token do Portalu Librus. + """ + auth_session = requests.session() + new_tokens = auth_session.post( + OAUTHURL, + data={ + 'grant_type': 'refresh_token', + 'refresh_token': self.refresh_token, + 'client_id': CLIENTID + } + ) + logging.debug('%s response %s', new_tokens.status_code, new_tokens.json()) + try: + self.root_token = new_tokens.json()['access_token'] + self.refresh_token = new_tokens.json()['refresh_token'] + except KeyError: + raise LibrusTricksAuthException('Invalid payload recived', new_tokens.json()) + + def revalidate_user(self): + """ + Aktualizuje token dostępu do Synergii, który wygasa po 24h. + """ + def do_revalidation(): + auth_session = requests.session() + new_token = auth_session.get( + FRESHURL.format(login=self.login), + headers={'Authorization': f'Bearer {self.root_token}'} + ) + logging.debug('%s response %s', new_token.status_code, new_token.json()) + return new_token + + new_token = do_revalidation() + if new_token.json().get('error') == 'access_denied': + logging.info('Obtaing new token failed! Refreshing root token') + self.revalidate_root() + new_token = do_revalidation() # again... + + try: + self.token = new_token.json()['accessToken'] + except KeyError: + raise LibrusTricksAuthException('Invalid response received', new_token.json()) + + def check_is_expired(self, use_clock=True, use_query=True): + """ + :param bool use_clock: Sprawdza na podstawie czasu + :param bool use_query: Sprawdza poprzez zapytanie http GET na ``/Me`` + :return: krotka z wynikami + :rtype: tuple[bool] + """ + clock_resp = None + query_resp = None + + if use_clock: + if datetime.now() > self.expires_in: + clock_resp = False + else: + clock_resp = True + if use_query: + test = requests.get('https://api.librus.pl/2.0/Me', headers={'Authorization': f'Bearer {self.token}'}) + if test.status_code == 401: + query_resp = False + else: + query_resp = True + + return clock_resp, query_resp + + @property + def is_valid(self): + """ + Umożliwia sprawdzenie czy konto ma jeszcze aktualny token. + + :return: ``False`` - trzeba wyrobić nowy token + :rtype: bool + """ + return self.check_is_expired(use_clock=False)[1] + + def dump_credentials(self, cred_file=None): + import json + if cred_file is None: + cred_file = open(f'{self.login}.json', 'w') + json.dump({ + 'user_dict': { + 'accessToken': self.token, + 'studentName': f'{self.name} {self.last_name}', + 'id': self.uid, + 'login': self.login, + }, + 'root_token': self.root_token, + 'revalidation_token': self.refresh_token, + 'exp_in': int(self.expires_in.timestamp()) + }, cred_file) + + def dict_credentials(self): + return { + 'user_dict': { + 'accessToken': self.token, + 'studentName': f'{self.name} {self.last_name}', + 'id': self.uid, + 'login': self.login, + }, + 'root_token': self.root_token, + 'revalidation_token': self.refresh_token, + 'exp_in': int(self.expires_in.timestamp()) + } + + +def load_json(cred_file): + import json + return SynergiaUser(**json.load(cred_file)) + + +def authorizer(email, password, user_agent=None): + """ + Zwraca listę użytkowników dostępnych dla danego konta Librus Portal + + :param str email: Email do Portalu Librus + :param str password: Hasło do Portalu Librus + :return: Listę z użytkownikami połączonymi do konta Librus Synergia + :rtype: list[librus_tricks.auth.SynergiaUser] + """ + if user_agent is None: + from random import choice + user_agent = choice([XIAOMI_USERAGENT, IPHONE_USERAGENT]) + logging.debug('No user-agent specified, using %s', user_agent) + + auth_session = requests.session() + auth_session.headers.update({'User-Agent': user_agent, 'X-Requested-With': 'pl.librus.synergiaDru2'}) + site = auth_session.get(LIBRUSLOGINURL) + soup = BeautifulSoup(site.text, 'html.parser') + csrf = soup.find('meta', attrs={'name': 'csrf-token'})['content'] + login_response_redirection = auth_session.post( + LOGINURL, json={'email': email, 'password': password}, + headers={'X-CSRF-TOKEN': csrf, 'Content-Type': 'application/json'} + ) + + if login_response_redirection.status_code != 200: + if login_response_redirection.status_code == 403: + if 'g-recaptcha-response' in login_response_redirection.json()['errors']: + raise CaptchaRequired(login_response_redirection.json()) + raise LibrusPortalInvalidPasswordError(login_response_redirection.json()) + raise LibrusLoginError(login_response_redirection.text) + + redirection_addr = login_response_redirection.json()['redirect'] + redirection_response = auth_session.get(redirection_addr, allow_redirects=False) + oauth_code = redirection_response.headers['location'].replace('http://localhost/bar?code=', '') + + synergia_root_response = auth_session.post( + OAUTHURL, + data={ + 'grant_type': 'authorization_code', + 'code': oauth_code, + 'client_id': CLIENTID, + 'redirect_uri': REDIRURL + } + ) + synergia_root_login_token = synergia_root_response.json()['access_token'] + synergia_root_revalidation_token = synergia_root_response.json()['refresh_token'] + synergia_root_expiration = synergia_root_response.json()['expires_in'] + + synergia_users_response = auth_session.get(SYNERGIAAUTHURL, + headers={'Authorization': f'Bearer {synergia_root_login_token}'}) + synergia_users_raw = synergia_users_response.json()['accounts'] + synergia_users = [ + SynergiaUser(user_data, synergia_root_login_token, synergia_root_revalidation_token, synergia_root_expiration) + for user_data in synergia_users_raw] + return synergia_users diff --git a/cache.py b/cache.py new file mode 100644 index 0000000..c9d3352 --- /dev/null +++ b/cache.py @@ -0,0 +1,134 @@ +from datetime import datetime + +from sqlalchemy import create_engine, String, JSON, Column, DateTime, Integer +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + + +class CacheBase: + def add_object(self, uid, cls, resource): + pass + + def get_object(self, uid, cls): + raise NotImplementedError('get_object require implementation') + + def del_object(self, uid): + raise NotImplementedError('del_object require implementation') + + def clear_objects(self): + raise NotImplementedError('clear_objects require implementation') + + def count_object(self): + raise NotImplementedError('count_object require implementation') + + def add_query(self, uri, response, user_id): + pass + + def get_query(self, uri, user_id): + raise NotImplementedError('get_query require implementation') + + def del_query(self, uri, user_id): + raise NotImplementedError('del_query require implementation') + + def clear_queries(self): + raise NotImplementedError('clear_queries require implementation') + + def count_queries(self): + raise NotImplementedError('count_queries require implementation') + + def about_backend(self): + raise NotImplementedError('required providing info about cache provider') + + +class DumbCache(CacheBase): + def get_object(self, uid, cls): + return + + def get_query(self, uri, user_id): + return + + def __repr__(self): + return f'' + + +class AlchemyCache(CacheBase): + Base = declarative_base() + + def __init__(self, engine_uri='sqlite:///:memory:'): + engine = create_engine(engine_uri, connect_args={'check_same_thread': False}, poolclass=StaticPool) + + db_session = sessionmaker(bind=engine) + db_session.configure(bind=engine) + self.session = db_session() + self.Base.metadata.create_all(engine) + self.syn_session = None + + class APIQueryCache(Base): + __tablename__ = 'uri_cache' + + pk = Column(Integer(), primary_key=True) + uri = Column(String(length=512)) + owner = Column(String(length=16)) + response = Column(JSON()) + last_load = Column(DateTime()) + + class ObjectLoadCache(Base): + __tablename__ = 'object_cache' + + uid = Column(Integer(), primary_key=True) + name = Column(String(length=64)) + resource = Column(JSON()) + last_load = Column(DateTime()) + + def add_object(self, uid, cls, resource): + self.session.add( + self.ObjectLoadCache(uid=uid, name=cls.__name__, resource=resource, last_load=datetime.now()) + ) + self.session.commit() + + def get_object(self, uid, cls): + """ + + :rtype: AlchemyCache.ObjectLoadCache + """ + response = self.session.query(self.ObjectLoadCache).filter_by(uid=uid, name=cls.__name__).first() + if response is None: + return None + return cls.assembly(response.resource, self.syn_session) + + def add_query(self, uri, response, user_id): + self.session.add( + self.APIQueryCache(uri=uri, response=response, last_load=datetime.now(), owner=user_id) + ) + self.session.commit() + + def get_query(self, uri, user_id): + """ + + :rtype: AlchemyCache.APIQueryCache + """ + return self.session.query(self.APIQueryCache).filter_by(uri=uri, owner=user_id).first() + + def del_query(self, uri, user_id): + self.session.query(self.APIQueryCache).filter_by(uri=uri, owner=user_id).delete() + self.session.commit() + + def del_object(self, uid): + self.session.query(self.ObjectLoadCache).filter_by(uid=uid).delete() + self.session.commit() + + def clear_queries(self): + self.session.query(self.APIQueryCache).delete() + + def clear_objects(self): + self.session.query(self.ObjectLoadCache).delete() + + def count_object(self): + return self.session.query(self.ObjectLoadCache).count() + + def about_backend(self): + return f'SQLAlchemy ORM {self.session.bind.dialect.name} {self.session.bind.dialect.driver}' + + def __repr__(self): + return f'<{self.__class__.__name__} with {self.session.bind.dialect.name} backend using {self.session.bind.dialect.driver} driver ({self.session.bind.url})>' diff --git a/classes.py b/classes.py new file mode 100644 index 0000000..057a6a1 --- /dev/null +++ b/classes.py @@ -0,0 +1,1080 @@ +from datetime import datetime, timedelta + +from librus_tricks.exceptions import SessionRequired, APIPathIsEmpty + + +class _RemoteObjectsUIDManager: + """ + Menadżer obiektów, które dopiero zostaną utworzone. + """ + + def __init__(self, session, parent): + """ + + :param librus_tricks.core.SynergiaClient session: Obiekt sesji + """ + self.__storage = dict() + self._session = session + self.__parent = parent + + def set_object(self, attr, uid, cls): + """ + Zapisuje dane przyszłego obiektu. + + :param str attr: Nazwa przyszłego property + :param int uid: Id obiektu + :param cls: Klasa obiektu + """ + self.__storage[attr] = uid, cls + # self.__parent.__setattr__(attr, cls.create(uid=uid, session=self.__session)) + return self + + def set_value(self, attr, val): + """ + Ustawia obiekt. + + :param str attr: Nazwa obiektu + :param val: Obiekt + """ + self.__storage[attr] = val + return self + + def assembly(self, attr): + """ + Pobiera wcześniej zapisany obiekt. + + :param str attr: Nazwa property + :return: Żądany obiekt + """ + uid, cls = self.__storage[attr] + return cls.create(uid=uid, session=self._session) + + def return_id(self, attr): + """ + Zwraca id obiektu. + + :param str attr: Nazwa property + :rtype: int + :return: Id obiektu + """ + return self.__storage[attr][0] + + +class SynergiaGenericClass: + """ + Klasa macierzysta dla obiektów dziennika Synergia. + """ + + def __init__(self, uid, resource, session): + """ + + :param str uid: Id żądanego obiektu + :param librus_tricks.core.SynergiaClient session: Obiekt sesji + :param resource: ścieżka do źródła danych + :type resource: iterable of str + :param str extraction_key: str zawierający klucz do wyjęcia danych + :param dict resource: dict zawierający gotowe dane (np. załadowane z cache) + """ + + self._session = session + self.uid = uid + self.objects = _RemoteObjectsUIDManager(self._session, self) + self._json_resource = resource + + # Of course i can comment it out, but for code completion props will be better + # def __getattr__(self, name): + # return self.objects_ids.assembly(name) + + @classmethod + def assembly(cls, resource, session): + """ + Umożliwia stworzenie obiektu posiadając dict i obiekt sesji. + + :param dict resource: Gotowe dane do stworzenia obiektu + :param librus_tricks.core.SynergiaClient session: Obiekt sesji + :return: Nowy obiekt + """ + self = cls(resource['Id'], resource, session) + return self + + @classmethod + def create(cls, uid=None, path=('',), session=None, extraction_key=None, expire=timedelta(seconds=1)): + """ + Pobiera i składa nowy obiekt. + + :param int uid: Id obiektu + :param tuple of str path: Niezłożona ścieżka API + :param librus_tricks.core.SynergiaClient session: Obiekt sesji + :param str extraction_key: Klucz do wyciągnięcia danych + :return: Pobrany obiekt + """ + import logging + + if uid is None or session is None: + raise SessionRequired() + + maybe_response = session.cache.get_object(uid, cls) + if not maybe_response is None: + logging.debug('Returning %s %s from object cache', maybe_response, uid) + return maybe_response + + if path == ('',): + raise APIPathIsEmpty(f'Path for {cls.__name__} class is empty!') + + response = session.get_cached_response(*path, uid, max_lifetime=expire) + logging.debug('Returning %s %s object from response cache', cls.__name__, uid) + + if extraction_key is None: + extraction_key = SynergiaGenericClass.auto_extract(response) + + resource = response[extraction_key] + self = cls(resource['Id'], resource, session) + return self + + @staticmethod + def auto_extract(payload): + """ + Próbuje automatycznie wydobyć klucz. + + :param dict payload: + :return: Wydobyty klucz + :rtype: str + """ + for key in payload.keys(): + if key not in ('Resources', 'Url'): + return key + return + + def export_resource(self): + return self._json_resource.copy() + + def __repr__(self): + return f'<{self.__class__.__name__} {self.uid} at {hex(id(self))}>' + + def __hash__(self): + return hash(self.uid) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.uid == other.uid + raise TypeError(f'Object is not instance of {self.__class__.__name__}') + + def __ne__(self, other): + return not self.__eq__(other) + + def _is_compatible(self, other): + if not isinstance(other, self.__class__): + raise TypeError() + return True + + +class SynergiaTeacher(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.name = self._json_resource['FirstName'] + self.last_name = self._json_resource['LastName'] + + @classmethod + def create(cls, uid=None, path=('Users',), session=None, extraction_key='User', expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.name} {self.last_name}>' + + def __str__(self): + return f'{self.name} {self.last_name}' + + +class SynergiaStudent(SynergiaTeacher): + pass + + +class SynergiaGlobalClass(SynergiaGenericClass): + """Klasa reprezentująca klasę (np. 1C)""" + + def __init__(self, uid, resource, session): + """ + Tworzy obiekt reprezentujący klasę (jako zbiór uczniów) + + :param str uid: id klasy + :param librus_tricks.core.SynergiaClient session: obiekt sesji z API Synergii + :param dict resource: dane z json'a + """ + super().__init__(uid, resource, session) + + self.alias = f'{self._json_resource["Number"]}{self._json_resource["Symbol"]}' + self.begin_date = datetime.strptime(self._json_resource['BeginSchoolYear'], '%Y-%m-%d').date() + self.end_date = datetime.strptime(self._json_resource['EndSchoolYear'], '%Y-%m-%d').date() + self.objects.set_object( + 'tutor', self._json_resource['ClassTutor']['Id'], SynergiaTeacher + ) + + @property + def tutor(self) -> SynergiaTeacher: + return self.objects.assembly('tutor') + + def __repr__(self): + return f'<{self.__class__.__name__} {self.alias}>' + + +class SynergiaVirtualClass(SynergiaGenericClass): + def __init__(self, uid, resource, session): + """ + Tworzy obiekt reprezentujący grupę uczniów + + :param str uid: id klasy + :param librus_tricks.core.SynergiaClient session: obiekt sesji z API Synergii + :param dict resource: dane z json'a + """ + super().__init__(uid, resource, session) + + self.name = self._json_resource['Name'] + self.number = self._json_resource['Number'] + self.symbol = self._json_resource['Symbol'] + self.objects.set_object( + 'teacher', self._json_resource['Teacher']['Id'], SynergiaTeacher + ).set_object( + 'subject', self._json_resource['Subject']['Id'], SynergiaSubject + ) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.name}>' + + @property + def teacher(self) -> SynergiaTeacher: + return self.objects.assembly('teacher') + + @property + def subject(self): + return self.objects.assembly('subject') + + +class SynergiaSubject(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.name = self._json_resource['Name'] + self.short_name = self._json_resource['Short'] + + @classmethod + def create(cls, uid=None, path=('Subjects',), session=None, extraction_key='Subject', expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.name}>' + + def __str__(self): + return self.name + + +class SynergiaLesson(SynergiaGenericClass): + def __init__(self, uid, resource, session): + """ + Klasa reprezentująca jednostkową lekcję + + :type session: librus_tricks.core.SynergiaClient + """ + super().__init__(uid, resource, session) + + self.objects.set_object( + 'teacher', self._json_resource['Teacher']['Id'], SynergiaTeacher + ).set_object( + 'subject', self._json_resource['Subject']['Id'], SynergiaSubject + ) + + @classmethod + def create(cls, uid=None, path=('Lessons',), session=None, extraction_key='Lesson', expire=timedelta(minutes=5)): + return super().create(uid, path, session, extraction_key, expire) + + @property + def teacher(self) -> SynergiaTeacher: + return self.objects.assembly('teacher') + + @property + def subject(self) -> SynergiaSubject: + return self.objects.assembly('subject') + + +class SynergiaGradeCategory(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + + def __try_to_extract(payload, extraction_key, false_return=None): + if extraction_key in payload.keys(): + return payload[extraction_key] + return false_return + + self.count_to_the_average = self._json_resource['CountToTheAverage'] + self.name = self._json_resource['Name'] + self.obligation_to_perform = self._json_resource['ObligationToPerform'] + self.standard = self._json_resource['Standard'] + self.weight = __try_to_extract(self._json_resource, 'Weight', false_return=0) + + if 'Teacher' in self._json_resource.keys(): + self.objects.set_object( + 'teacher', self._json_resource['Id'], SynergiaTeacher + ) + + @classmethod + def create(cls, uid=None, path=('Grades', 'Categories'), session=None, extraction_key='Category', + expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + @property + def teacher(self): + try: + return self.objects.assembly('teacher') + except KeyError: + return + + def __repr__(self): + return f'<{self.__class__.__name__} {self.name}>' + + +class SynergiaGradeComment(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + + self.text = self._json_resource['Text'] + self.objects.set_object( + 'teacher', self._json_resource['AddedBy']['Id'], SynergiaTeacher + ).set_object( + 'bind', self._json_resource['Grade']['Id'], SynergiaGrade + ) + + def __str__(self): + return self.text + + @classmethod + def create(cls, uid=None, path=('Grades', 'Comments'), session=None, extraction_key='Comment', + expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + @property + def teacher(self) -> SynergiaTeacher: + return self.objects.assembly('teacher') + + @property + def grade_bind(self): + return self.objects.assembly('bind') + + def __repr__(self): + return f'<{self.__class__.__name__} {self.text}>' + + +class SynergiaBaseTextGrade(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + + self.add_date = datetime.strptime(self._json_resource['AddDate'], '%Y-%m-%d %H:%M:%S') + self.date = datetime.strptime(self._json_resource['Date'], '%Y-%m-%d').date() + self.grade = self._json_resource['Grade'] + self.semester = self._json_resource['Semester'] + self.visible = self._json_resource['ShowInGradesView'] + self.objects.set_object( + 'teacher', self._json_resource['AddedBy']['Id'], SynergiaTeacher + ).set_object( + 'subject', self._json_resource['Subject']['Id'], SynergiaSubject + ).set_object( + 'student', self._json_resource['Student']['Id'], SynergiaStudent + ) + + @classmethod + def create(cls, uid=None, path=('BaseTextGrades',), session=None, extraction_key='BaseTextGrades', + expire=timedelta(minutes=5)): + return super().create(uid, path, session, extraction_key, expire) + + @property + def teacher(self) -> SynergiaTeacher: + return self.objects.assembly('teacher') + + @property + def subject(self) -> SynergiaSubject: + return self.objects.assembly('subject') + + @property + def student(self): + return self.objects.assembly('student') + + +class SynergiaGrade(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + + class GradeMetadata: + def __init__(self, is_c, is_s, is_sp, is_f, is_fp): + self.is_constituent = is_c + self.is_semester_grade = is_s + self.is_semester_grade_proposition = is_sp + self.is_final_grade = is_f + self.is_final_grade_proposition = is_fp + + self.add_date = datetime.strptime(self._json_resource['AddDate'], '%Y-%m-%d %H:%M:%S') + self.date = datetime.strptime(self._json_resource['Date'], '%Y-%m-%d').date() + self.grade = self._json_resource['Grade'] + self.is_constituent = self._json_resource['IsConstituent'] + self.semester = self._json_resource['Semester'] + self.metadata = GradeMetadata( + self._json_resource['IsConstituent'], + self._json_resource['IsSemester'], + self._json_resource['IsSemesterProposition'], + self._json_resource['IsFinal'], + self._json_resource['IsFinalProposition'] + ) + + self.objects.set_object( + 'teacher', self._json_resource['AddedBy']['Id'], SynergiaTeacher + ).set_object( + 'subject', self._json_resource['Subject']['Id'], SynergiaSubject + ).set_object( + 'category', self._json_resource['Category']['Id'], SynergiaGradeCategory + ) + + @property + def is_special(self): + m = self.metadata + if m.is_final_grade or m.is_final_grade_proposition or m.is_semester_grade or m.is_semester_grade_proposition: + return True + return False + + def __repr__(self): + return f'<{self.__class__.__name__} {self.grade} from SynergiaSubject with id {self.objects.return_id("subject")} ' \ + f'added {self.add_date.strftime("%Y-%m-%d %H:%M:%S")}>' + + def __str__(self): + return self.grade + + @property + def teacher(self) -> SynergiaTeacher: + return self.objects.assembly('teacher') + + @property + def subject(self) -> SynergiaSubject: + return self.objects.assembly('subject') + + @property + def category(self) -> SynergiaGradeCategory: + return self.objects.assembly('category') + + @property + def comments(self): + """ + + :rtype: list of SynergiaGradeComment + """ + if self._json_resource.get('Comments') is not None: + return [ + SynergiaGradeComment.create( + uid=com.get('Id'), session=self._session + ) for com in self._json_resource.get('Comments') + ] + return tuple() + + @property + def real_value(self): + return { + '1': 1, + '1+': 1.5, + '2-': 1.75, + '2': 2, + '2+': 2.5, + '3-': 2.75, + '3': 3, + '3+': 3.5, + '4-': 3.75, + '4': 4, + '4+': 4.5, + '5-': 4.75, + '5': 5, + '5+': 5.5, + '6-': 5.75, + '6': 6 + }.get(self.grade) + + @classmethod + def create(cls, uid=None, path=('Grades',), session=None, extraction_key='Grade', expire=timedelta(minutes=45)): + return super().create(uid, path, session, extraction_key, expire) + + def __eq__(self, other): + self._is_compatible(other) + return self.real_value == other.real_value and self.category.weight + + def __gt__(self, other): + self._is_compatible(other) + return self.real_value > other.real_value and self.category.weight >= other.category.weight + + def __ge__(self, other): + self._is_compatible(other) + return self.real_value >= other.real_value and self.category.weight >= other.category.weight + + def __lt__(self, other): + self._is_compatible(other) + return self.real_value < other.real_value and self.category.weight <= other.category.weight + + def __le__(self, other): + self._is_compatible(other) + return self.real_value <= other.real_value and self.category.weight <= other.category.weight + + +class SynergiaAttendanceType(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.color = self._json_resource['ColorRGB'] + self.is_presence_kind = self._json_resource['IsPresenceKind'] + self.name = self._json_resource['Name'] + self.short_name = self._json_resource['Short'] + + @classmethod + def create(cls, uid=None, path=('Attendances', 'Types'), session=None, extraction_key='Type', + expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.short_name}>' + + def __str__(self): + return self.name + + +class SynergiaAttendance(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.add_date = datetime.strptime(self._json_resource['AddDate'], '%Y-%m-%d %H:%M:%S') + self.date = datetime.strptime(self._json_resource['Date'], '%Y-%m-%d').date() + self.lesson_no = int(self._json_resource['LessonNo']) + self.objects.set_object( + 'teacher', self._json_resource['AddedBy']['Id'], SynergiaTeacher + ).set_object( + 'student', self._json_resource['Student']['Id'], SynergiaStudent + ).set_object( + 'type', self._json_resource['Type']['Id'], SynergiaAttendanceType + ).set_object( + 'lesson', resource['Lesson']['Id'], SynergiaLesson + ) + + @classmethod + def create(cls, uid=None, path=('Attendances',), session=None, extraction_key='Attendance', + expire=timedelta(minutes=10)): + return super().create(uid, path, session, extraction_key, expire) + + @property + def teacher(self): + """ + :rtype: SynergiaTeacher + """ + return self.objects.assembly('teacher') + + @property + def student(self): + """ + :rtype: SynergiaStudent + """ + return self.objects.assembly('student') + + @property + def type(self): + """ + :rtype: SynergiaAttendanceType + """ + return self.objects.assembly('type') + + @property + def lesson(self): + """ + :rtype: librus_tricks.classes.SynergiaLesson + """ + return self.objects.assembly('lesson') + + def __repr__(self): + return f'' + + def __str__(self): + return self.type.__str__() + + +class SynergiaExamCategory(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + + self.name = self._json_resource['Name'] + self.objects.set_object('color', self._json_resource['Color']['Id'], SynergiaColor) + + @classmethod + def create(cls, uid=None, path=('HomeWorks', 'Categories'), session=None, extraction_key='Category', + expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + @property + def color(self): + """ + + :rtype: SynergiaColor + """ + return self.objects.assembly('color') + + def __str__(self): + return self.name + + +class SynergiaExam(SynergiaGenericClass): + def __init__(self, uid, resource, session): + + super().__init__(uid, resource, session) + + self.add_date = datetime.strptime(self._json_resource['AddDate'], '%Y-%m-%d %H:%M:%S') + self.content = self._json_resource['Content'] + self.date = datetime.strptime(self._json_resource['Date'], '%Y-%m-%d').date() + self.lesson = self._json_resource['LessonNo'] + if self._json_resource['TimeFrom'] is None: + self.time_start = None + else: + self.time_start = datetime.strptime(self._json_resource['TimeFrom'], '%H:%M:%S').time() + if self._json_resource['TimeTo'] is None: + self.time_end = None + else: + self.time_end = datetime.strptime(self._json_resource['TimeTo'], '%H:%M:%S').time() + + self.objects.set_object( + 'teacher', self._json_resource['CreatedBy']['Id'], SynergiaTeacher + ).set_object( + 'category', self._json_resource['Category']['Id'], SynergiaExamCategory + ) + if 'Subject' in self._json_resource: + self.objects.set_object('subject', self._json_resource['Subject']['Id'], SynergiaSubject) + self.__subject_present = True + else: + self.__subject_present = False + + @classmethod + def create(cls, uid=None, path=('HomeWorks',), session=None, extraction_key='HomeWork', expire=timedelta(days=3)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} ' \ + f'{self.date.strftime("%Y-%m-%d")} for subject {self.subject}>' + + @property + def teacher(self) -> SynergiaTeacher: + return self.objects.assembly('teacher') + + # @property + # def group(self): + # """ + # + # :rtype: SynergiaGlobalClass + # :rtype: SynergiaVirtualClass + # """ + # if self.objects_ids.group_type is SynergiaGlobalClass: + # return SynergiaGlobalClass(self.objects_ids.group, self._session) + # else: + # return SynergiaVirtualClass(self.objects_ids.group, self._session) + + @property + def subject(self): + if self.__subject_present: + return self.objects.assembly('subject') + return None + + @property + def category(self): + return self.objects.assembly('category') + + +class SynergiaColor(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.name = self._json_resource['Name'] + self.hex_rgb = self._json_resource['RGB'] + + @classmethod + def create(cls, uid=None, path=('Colors',), session=None, extraction_key='Color', expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.hex_rgb}>' + + def __str__(self): + return self.hex_rgb + + +class SynergiaClassroom(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.name = self._json_resource['Name'] + self.symbol = self._json_resource['Symbol'] + + @classmethod + def create(cls, uid=None, path=('Classrooms',), session=None, extraction_key=None, expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'' + + def __str__(self): + return self.name + + +class SynergiaTeacherFreeDaysTypes(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.name = self._json_resource[0]['Name'] + + +class SynergiaTeacherFreeDays(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + + self.starts = datetime.strptime(self._json_resource['DateFrom'], '%Y-%m-%d').date() + self.ends = datetime.strptime(self._json_resource['DateTo'], '%Y-%m-%d').date() + self.objects.set_object( + 'teacher', self._json_resource['Teacher']['Id'], SynergiaTeacher + ) + + if self._json_resource.get('TimeTo') is not None: + self.time_begin = datetime.strptime(self._json_resource['TimeFrom'], '%H:%M:%S').time() + self.time_ends = datetime.strptime(self._json_resource['TimeTo'], '%H:%M:%S').time() + else: + self.time_begin = None + self.time_ends = None + + @property + def period_length(self): + return self.ends - self.starts + + @property + def teacher(self) -> SynergiaTeacher: + """ + + :rtype: SynergiaTeacher + """ + return self.objects.assembly('teacher') + + def __repr__(self): + return f'' + + +class SynergiaSchoolFreeDays(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.starts = datetime.strptime(self._json_resource['DateFrom'], '%Y-%m-%d').date() + self.ends = datetime.strptime(self._json_resource['DateTo'], '%Y-%m-%d').date() + self.name = self._json_resource['Name'] + + @classmethod + def create(cls, uid=None, path=('Calendars', 'SchoolFreeDays'), session=None, extraction_key='SchoolFreeDays', + expire=timedelta(minutes=5)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.starts.isoformat()} - {self.ends.isoformat()}>' + + +class SynergiaTimetableEntry(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.available = datetime.strptime(resource['DateFrom'], '%Y-%m-%d').date(), \ + datetime.strptime(resource['DateTo'], '%Y-%m-%d').date() + + @classmethod + def create(cls, uid=None, path=('TimetableEntries',), session=None, extraction_key='TimetableEntry', + expire=timedelta(seconds=15)): + return super().create(uid, path, session, extraction_key, expire) + + +class SynergiaTimetableEvent: + def __init__(self, resource, session): + self.lesson_no = int(resource['LessonNo']) #: int: numer lekcji + self.start = datetime.strptime(resource['HourFrom'], '%H:%M').time() #: time: początek lekcji + self.end = datetime.strptime(resource['HourTo'], '%H:%M').time() #: time: koniec lekcji + self.is_cancelled = resource['IsCanceled'] #: bool: czy lekcja jest odwołana + self.is_sub = resource['IsSubstitutionClass'] #: bool: czy lekcja jest zastępstwem + self.preloaded = { + 'subject_title': resource['Subject']['Name'], + 'teacher': f'{resource["Teacher"]["FirstName"]} {resource["Teacher"]["LastName"]}' + } + self.objects = _RemoteObjectsUIDManager(session, self) + self.objects.set_object( + 'subject', resource['Subject']['Id'], SynergiaSubject + ).set_object( + 'teacher', resource['Teacher']['Id'], SynergiaTeacher + ) + + self.__set_classroom(resource) + + def __set_classroom(self, resource): + """ + + :param dict resource: + :return: + """ + if 'Classroom' in resource.keys(): + self.objects.set_object( + 'classroom', resource['Classroom']['Id'], SynergiaClassroom + ) + elif 'OrgClassroom' in resource.keys(): + self.objects.set_object( + 'classroom', resource['OrgClassroom']['Id'], SynergiaClassroom + ) + else: + self.objects.set_value('classroom', None) + + @property + def lesson_status(self): + if self.is_cancelled: + return 'Cancelled' + elif self.is_sub: + return 'Changed' + return 'Planned' + + @property + def subject(self): + """ + :rtype: librus_tricks.classes.SynergiaSubject + """ + return self.objects.assembly('subject') + + @property + def teacher(self): + """ + :rtype: librus_tricks.classes.SynergiaTeacher + """ + return self.objects.assembly('teacher') + + @property + def classroom(self): + """ + :rtype: librus_tricks.classes.SynergiaClassroom + """ + return self.objects.assembly('classroom') + + @property + def human_readable_time_range(self): + return f'{self.start.strftime("%H:%M")} - {self.end.strftime("%H:%M")}' + + def __repr__(self): + return f'' + + def __str__(self): + return self.preloaded["subject_title"] + + +class SynergiaTimetableDay: + def __init__(self, lessons): + self.lessons = tuple(lessons) #: tuple[SynergiaTimetableEvent]: krotka z lekcjami + if self.lessons.__len__() != 0: + self.day_start = self.lessons[0].start + self.day_end = self.lessons[-1].end + else: + self.day_start = None + self.day_end = None + + def __repr__(self): + return f'<{self.__class__.__name__} with {self.lessons.__len__()} lessons between {self.day_start} and {self.day_end}>' + + +class SynergiaTimetable(SynergiaGenericClass): + """ + Obiekt zawierający cały tydzień w planie lekcji + """ + + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.days = self.convert_parsed_timetable( + self.parse_timetable(resource) + ) #: list[SynergiaTimetableDay]: lista z dniami tygodnia + + @property + def today_timetable(self): + """ + + :rtype: list of SynergiaTimetableEvent + """ + return self.days[datetime.now().date()] + + @classmethod + def assembly(cls, resource, session): + pseudo_id = int(datetime.now().timestamp()).__str__() + self = cls(pseudo_id, resource, session) + return self + + @classmethod + def create(cls, uid=None, path=('Timetables',), session=None, extraction_key='Timetable', + expire=timedelta(seconds=15)): + response = session.get_cached_response(*path) + + if extraction_key is None: + extraction_key = SynergiaGenericClass.auto_extract(response) + + resource = response[extraction_key] + self = cls.assembly(resource, session) + return self + + @staticmethod + def parse_timetable(resource): + root = {} + + for day in resource.keys(): + day_date = datetime.strptime(day, '%Y-%m-%d').date() + root[day_date] = [] + for period in resource[day]: + if period.__len__() != 0: + root[day_date].append(period[0]) + return root + + def convert_parsed_timetable(self, timetable): + for day in timetable: + for event_index in range(len(timetable[day])): + if timetable[day][event_index].keys().__len__() != 0: + timetable[day][event_index] = SynergiaTimetableEvent(timetable[day][event_index], self._session) + + for day in timetable.keys(): + timetable[day] = SynergiaTimetableDay(timetable[day]) + + return timetable + + def __repr__(self): + return f'<{self.__class__.__name__} for {self.days.keys()}>' + + def __str__(self): + o_str = '' + for day_key in self.days.keys(): + o_str += f'{day_key}\n' + for event in self.days[day_key]: + if event != {}: + o_str += f' {event.__str__()}\n' + return o_str + + +class SynergiaNativeMessageAuthor(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.name = resource.get('Name') + + @classmethod + def create(cls, uid=None, path=('Messages', 'User'), session=None, extraction_key='User', + expire=timedelta(days=60)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.name}>' + + @property + def matching_indentity(self): + teachers = self._session.return_objects('Users', cls=SynergiaTeacher, extraction_key='Users') + for teacher in teachers: + if str(teacher.name) in self.name and str(teacher.last_name) in self.name: + return teacher + return + + +class SynergiaNativeMessage(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.body = self._json_resource['Body'] #: str: wiadomość + self.topic = self._json_resource['Subject'] #: str: temat + self.send_date = datetime.fromtimestamp(self._json_resource['SendDate']) #: datetime: data wysłania + self.objects.set_object('sender', self._json_resource['Sender']['Id'], SynergiaNativeMessageAuthor) + + @property + def sender(self) -> SynergiaNativeMessageAuthor: + return self.objects.assembly('sender') + + @classmethod + def create(cls, uid=None, path=('Messages',), session=None, extraction_key='Message', expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + +class SynergiaNews(SynergiaGenericClass): + """ + Obiekt reprezentujący ogłoszenie szkolne + """ + + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.content = self._json_resource['Content'] #: str: wiadomość ogłoszenia + self.created = datetime.strptime(self._json_resource['CreationDate'], + '%Y-%m-%d %H:%M:%S') #: datetime: data utworzenia + self.unique_id = self._json_resource['Id'] #: int: id ogłoszenia + self.topic = self._json_resource['Subject'] #: str: temat + self.was_read = self._json_resource['WasRead'] #: bool: status odczytania? + self.starts = datetime.strptime(self._json_resource['StartDate'], '%Y-%m-%d') #: date: ?? + self.ends = datetime.strptime(self._json_resource['EndDate'], '%Y-%m-%d') #: date: ?? + self.objects.set_object( + 'teacher', self._json_resource['AddedBy']['Id'], SynergiaTeacher + ) + + @property + def teacher(self) -> SynergiaTeacher: + return self.objects.assembly('teacher') + + @classmethod + def create(cls, uid=None, path=('SchoolNotices',), session=None, extraction_key='SchoolNotices', + expire=timedelta(days=31)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'' + + +class SynergiaSchool(SynergiaGenericClass): + """ + Obiekt zawierający informacje o szkole + """ + + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.name = resource['Name'] #: str: nazwa szkoły + self.location = f'{resource["Town"]} {resource["Street"]} {resource["BuildingNumber"]}' #: str: adres szkoły + + @classmethod + def create(cls, uid=None, path=('Schools',), session=None, extraction_key='School', expire=timedelta(seconds=1)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.name}>' + + +class SynergiaSubstitution(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + + @classmethod + def create(cls, uid=None, path=('Calendars', 'Substitutions'), session=None, extraction_key='Substitution', + expire=timedelta(days=7)): + return super().create(uid, path, session, extraction_key, expire) + + +class SynergiaRealization(SynergiaGenericClass): + def __init__(self, uid, resource, session): + super().__init__(uid, resource, session) + self.topic = resource['Topic'] + self.date = datetime.strptime(resource['Date'], '%Y-%m-%d') + self.is_trip = resource['IsTrip'] + self.teaching_program = resource.get('TeachingProgramTopic') + self.lesson_no = resource['LessonNo'] + self.objects.set_object( + 'teacher', self._json_resource['AddedBy']['Id'], SynergiaTeacher + ).set_object( + 'lesson', resource['Lesson']['Id'], SynergiaLesson + ) + + @property + def teacher(self): + """ + :rtype: librus_tricks.classes.SynergiaTeacher + """ + return self.objects.assembly('teacher') + + @property + def lesson(self): + """ + :rtype: librus_tricks.classes.SynergiaLesson + """ + return self.objects.assembly('lesson') + + @classmethod + def create(cls, uid=None, path=('Realizations',), session=None, extraction_key='Realization', + expire=timedelta(seconds=1)): + return super().create(uid, path, session, extraction_key, expire) + + def __repr__(self): + return f'<{self.__class__.__name__} {self.topic}>' diff --git a/core.py b/core.py new file mode 100644 index 0000000..609fecc --- /dev/null +++ b/core.py @@ -0,0 +1,483 @@ +import logging +from datetime import timedelta + +import requests + +from librus_tricks import cache as cache_lib +from librus_tricks import exceptions, tools +from librus_tricks.classes import * +from librus_tricks.messages import MessageReader + + +class SynergiaClient: + """Sesja z API Synergii""" + + def __init__(self, user, api_url='https://api.librus.pl/2.0', user_agent='LibrusMobileApp', + cache=cache_lib.AlchemyCache()): + """ + Tworzy sesję z API Synergii. + + :param librus_tricks.auth.SynergiaUser user: Użytkownik sesji + :param str api_url: Bazowy url api, zmieniaj jeżeli chcesz używać proxy typu beeceptor + :param str user_agent: User-agent klienta http, domyślnie się podszywa pod aplikację + :param librus_tricks.cache.CacheBase cache: Obiekt, który zarządza cache + """ + self.user = user + self.session = requests.session() + + self.session.headers.update({'User-Agent': user_agent}) + self.__auth_headers = {'Authorization': f'Bearer {user.token}'} + self.__api_url = api_url + + if cache_lib.CacheBase in cache.__class__.__bases__: + self.cache = cache + self.li_session = self + else: + raise exceptions.InvalidCacheManager(f'{cache} can not be a cache object!') + + self.__message_reader = None + + @property + def message_reader(self): + if self.__message_reader is None: + self.__message_reader = MessageReader(self) + return self.__message_reader + + def __repr__(self): + return f'' + + def __update_auth_header(self): + self.__auth_headers = {'Authorization': f'Bearer {self.user.token}'} + logging.debug('Updating headers to %s', self.__auth_headers) + + @staticmethod + def assembly_path(*elements, prefix='', suffix='', sep='/'): + """ + Składa str w jednego str, przydatne przy tworzeniu url. + + :param str elements: Elementy do stworzenia str + :param str prefix: Początek str + :param str suffix: Koniec str + :param str sep: str wstawiany pomiędzy elementy + :return: Złożony str + :rtype: str + """ + for element in elements: + prefix += sep + str(element) + return prefix + suffix + + # HTTP part + + def dispatch_http_code(self, response: requests.Response, callback=None, callback_args=tuple(), + callback_kwargs=None): + """ + Sprawdza czy serwer zgłasza błąd poprzez podanie kodu http, w przypadku błędu, rzuca wyjątkiem. + + :param requests.Response response: + :raises librus_tricks.exceptions.SynergiaNotFound: 404 + :raises librus_tricks.exceptions.SynergiaForbidden: 403 + :raises librus_tricks.exceptions.SynergiaAccessDenied: 401 + :raises librus_tricks.exceptions.SynergiaInvalidRequest: 401 + :rtype: requests.Response + :return: sprawdzona odpowiedź http + """ + if callback_kwargs is None: + callback_kwargs = dict() + + logging.debug('Dispatching response status') + if response.json().get('Code') == 'TokenIsExpired': + logging.info('Server returned error code "TokenIsExpired", trying to obtain new token') + self.user.revalidate_user() + self.__update_auth_header() + logging.debug('Repeating failed response') + return callback(*callback_args, **callback_kwargs) + + logging.debug('Dispatching http status code') + if response.status_code >= 400: + try: + raise { + 503: exceptions.SynergiaMaintenanceError(response.url, response.json()), + 500: exceptions.SynergiaServerError(response.url, response.json()), + 404: exceptions.SynergiaAPIEndpointNotFound(response.url), + 403: exceptions.SynergiaForbidden(response.url, response.json()), + 401: exceptions.SynergiaAccessDenied(response.url, response.json()), + 400: exceptions.SynergiaAPIInvalidRequest(response.url, response.json()), + }[response.status_code] + except KeyError: + raise exceptions.OtherHTTPResponse('Not excepted HTTP error code!', response.status_code) + + return response.json() + + def get(self, *path, request_params=None): + """ + Wykonuje odpowiednio spreparowane zapytanie http GET. + + :param path: Ścieżka zawierająca węzeł API + :type path: str + :param request_params: dict zawierający kwargs dla zapytania http + :type request_params: dict + :return: json przekonwertowany na dict'a + :rtype: dict + """ + if request_params is None: + request_params = dict() + path_str = self.assembly_path(*path, prefix=self.__api_url) + response = self.session.get( + path_str, headers=self.__auth_headers, params=request_params + ) + + response = self.dispatch_http_code(response, callback=self.get, callback_args=path, + callback_kwargs=request_params) + + return response + + def post(self, *path, request_params=None): + """ + Pozwala na dokonanie zapytania http POST. + + :param path: Ścieżka zawierająca węzeł API + :type path: str + :param request_params: dict zawierający kwargs dla zapytania http + :type request_params: dict + :return: json przekonwertowany na dict'a + :rtype: dict + """ + if request_params is None: + request_params = dict() + path_str = self.assembly_path(*path, prefix=self.__api_url) + response = self.session.post( + path_str, headers=self.__auth_headers, params=request_params + ) + + response = self.dispatch_http_code(response, callback=self.post, callback_args=path, + callback_kwargs=request_params) + + return response + + # Cache + + def get_cached_response(self, *path, http_params=None, max_lifetime=timedelta(hours=1)): + """ + Wykonuje zapytanie http GET z poprzednim sprawdzeniem cache. + + :param path: Niezłożona ścieżka do węzła API + :param http_params: dict zawierający kwargs dla zapytania http + :type http_params: dict + :param timedelta max_lifetime: Maksymalny czas ważności cache dla tego zapytania http + :return: dict zawierający odpowiedź zapytania + :rtype: dict + """ + uri = self.assembly_path(*path, prefix=self.__api_url) + response_cached = self.cache.get_query(uri, self.user.uid) + + if response_cached is None: + logging.debug('Response is not present in cache!') + http_response = self.get(*path, request_params=http_params) + self.cache.add_query(uri, http_response, self.user.uid) + return http_response + + try: + age = datetime.now() - response_cached.last_load + except TypeError: + age = datetime.now() - response_cached.last_load.replace(tzinfo=None) + + if age > max_lifetime: + logging.debug('Response is too old! Trying to get latest response from api') + http_response = self.get(*path, request_params=http_params) + self.cache.del_query(uri, self.user.uid) + self.cache.add_query(uri, http_response, self.user.uid) + return http_response + return response_cached.response + + def get_cached_object(self, uid, cls, max_lifetime=timedelta(hours=1)): + """ + Pobiera dany obiekt z poprzednim sprawdzeniem cache. Nie używane domyślnie w bibliotece. + + :param str uid: Id żądanego obiektu + :param cls: Klasa żądanego obiektu + :param timedelta max_lifetime: Maksymalny czas ważności cache dla tego obiektu + :return: Żądany obiekt + """ + requested_object = self.cache.get_object(uid, cls) + + if requested_object is None: + logging.debug('Obejct is not present in cache!') + requested_object = cls.create(uid=uid, session=self) + self.cache.add_object(uid, cls, requested_object.export_resource()) + return requested_object + + try: + age = datetime.now() - requested_object.last_load + except TypeError: + age = datetime.now() - requested_object.last_load.replace(tzinfo=None) + + if age > max_lifetime: + logging.debug('Object is too old! Trying to get latest object from api') + requested_object = cls.create(uid=uid, session=self) + self.cache.del_object(uid) + self.cache.add_object(uid, cls, requested_object.export_resource()) + + return requested_object + + # API query part + + def return_objects(self, *path, cls, extraction_key=None, lifetime=timedelta(seconds=10), bypass_cache=False): + """ + Zwraca listę obiektów lub obiekt, wygenerowaną z danych danej ścieżki API. + + :param str path: Niezłożona ścieżka do węzła API + :param cls: Klasa żądanych obiektów + :param str extraction_key: Klucz do wyjęcia danych, pozostawienie tego parametru na None powoduje + automatyczną próbę zczytania danych + :param timedelta lifetime: Maksymalny czas ważności cache dla tego zapytania http + :param bool bypass_cache: Ustawienie tego parametru na True powoduje ignorowanie mechanizmu cache + :return: Lista żądanych obiektów + :rtype: list[SynergiaGenericClass] + """ + if bypass_cache: + raw = self.get(*path) + else: + raw = self.get_cached_response(*path, max_lifetime=lifetime) + + if extraction_key is None: + extraction_key = SynergiaGenericClass.auto_extract(raw) + + raw = raw[extraction_key] + + if isinstance(raw, list): + stack = [] + for stored_payload in raw: + stack.append(cls.assembly(stored_payload, self)) + return tuple(stack) + if isinstance(raw, dict): + return cls.assembly(raw, self) + + return None + + def grades(self, *grades): + """ + :param int grades: Id ocen + :rtype: tuple[librus_tricks.classes.SynergiaGrade] + :return: krotka z wszystkimi/wybranymi ocenami + """ + if grades.__len__() == 0: + return self.return_objects('Grades', cls=SynergiaGrade, extraction_key='Grades') + ids_computed = self.assembly_path(*grades, sep=',', suffix=',')[1:] + return self.return_objects('Grades', ids_computed, cls=SynergiaGrade, extraction_key='Grades') + + @property + def grades_categorized(self): + grades_categorized = {} + for subject in self.subjects(): + grades_categorized[subject.name] = [] + + for grade in self.grades(): + grades_categorized[grade.subject.name].append( + grade + ) + + for subjects in grades_categorized.copy().keys(): + if grades_categorized[subjects].__len__() == 0: + del (grades_categorized[subjects]) + + return grades_categorized + + def attendances(self, *attendances): + """ + :param int attendances: Id obecności + :rtype: tuple[librus_tricks.classes.SynergiaAttendance] + :return: krotka z wszystkimi/wybranymi obecnościami + """ + if attendances.__len__() == 0: + return self.return_objects('Attendances', cls=SynergiaAttendance, extraction_key='Attendances') + ids_computed = self.assembly_path(*attendances, sep=',', suffix=',')[1:] + return self.return_objects('Attendances', ids_computed, cls=SynergiaAttendance, extraction_key='Attendances') + + @property + def illegal_absences(self): + """ + :rtype: tuple[librus_tricks.classes.SynergiaAttendance] + :return: krotka z nieusprawiedliwionymi nieobecnościami + """ + + def is_absence(k): + if k.type.uid == '1': + return True + return False + + return tuple(filter(is_absence, self.attendances())) + + @property + def all_absences(self): + """ + :rtype: tuple[librus_tricks.classes.SynergiaAttendance] + :return: krotka z wszystkimi nieobecnościami + """ + return tuple(filter(lambda k: not k.type.is_presence_kind, self.attendances())) + + def exams(self, *exams): + """ + :param int exams: Id egzaminów + :rtype: tuple[librus_tricks.classes.SynergiaExam] + :return: krotka z wszystkimi egzaminami + """ + if exams.__len__() == 0: + return self.return_objects('HomeWorks', cls=SynergiaExam, extraction_key='HomeWorks') + ids_computed = self.assembly_path(*exams, sep=',', suffix=',')[1:] + return self.return_objects('HomeWorks', ids_computed, cls=SynergiaExam, extraction_key='HomeWorks') + + def colors(self, *colors): + """ + :param int colors: Id kolorów + :rtype: tuple[librus_tricks.classes.SynergiaColors] + """ + if colors.__len__() == 0: + return self.return_objects('Colors', cls=SynergiaColor, extraction_key='Colors') + ids_computed = self.assembly_path(*colors, sep=',', suffix=',')[1:] + return self.return_objects('Colors', ids_computed, cls=SynergiaColor, extraction_key='Colors') + + def timetable(self, for_date=datetime.now()): + """ + Plan lekcji na cały tydzień. + + :param datetime.datetime for_date: Data dnia, który ma być w planie lekcji + :rtype: librus_tricks.classes.SynergiaTimetable + :return: obiekt tygodniowego planu lekcji + """ + monday = tools.get_actual_monday(for_date).isoformat() + matrix = self.get('Timetables', request_params={'weekStart': monday}) + return SynergiaTimetable.assembly(matrix['Timetable'], self) + + def timetable_day(self, for_date: datetime): + return self.timetable(for_date).days[for_date.date()] + + @property + def today_timetable(self): + """ + Plan lekcji na dzisiejszy dzień. + + :rtype: librus_tricks.classes.SynergiaTimetableDay + :return: Plan lekcji na dziś + """ + try: + return self.timetable().days[datetime.now().date()] + except KeyError: + return None + + @property + def tomorrow_timetable(self): + """ + Plan lekcji na kolejny dzień. + + :rtype: librus_tricks.classes.SynergiaTimetableDay + :return: Plan lekcji na jutro + """ + try: + return self.timetable(datetime.now() + timedelta(days=1)).days[(datetime.now() + timedelta(days=1)).date()] + except KeyError: + return None + + def messages(self, *messages): + """ + Wymaga mobilnych dodatków. + + :param int messages: Id wiadomości + :rtype: tuple[librus_tricks.classes.SynergiaNativeMessage] + """ + if messages.__len__() == 0: + return self.return_objects('Messages', cls=SynergiaNativeMessage, extraction_key='Messages') + ids_computed = self.assembly_path(*messages, sep=',', suffix=',')[1:] + return self.return_objects('Messages', ids_computed, cls=SynergiaNativeMessage, extraction_key='Messages') + + def news_feed(self): + """ + :return: Wszystkie ogłoszenia szkolne + :rtype: tuple[librus_tricks.classes.SynergiaNews] + """ + return self.return_objects('SchoolNotices', cls=SynergiaNews, extraction_key='SchoolNotices') + + def subjects(self, *subject): + """ + :return: Wszystkie/wybrane przedmioty lekcyjne + :param int subject: Id przedmiotów + :rtype: tuple[librus_tricks.classes.SynergiaSubject] + """ + if subject.__len__() == 0: + return self.return_objects('Subjects', cls=SynergiaSubject, extraction_key='Subjects') + ids_computed = self.assembly_path(*subject, sep=',', suffix=',')[1:] + return self.return_objects('Subjects', ids_computed, cls=SynergiaSubject, extraction_key='Subjects') + + @property + def school(self): + """ + :return: Obiekt z informacjami o twojej szkole + :rtype: librus_tricks.classes.SynergiaSchool + """ + return self.return_objects('Schools', cls=SynergiaSchool, extraction_key='School') + + @property + def lucky_number(self): + """ + :return: Szczęśliwy numerek + :rtype: int + """ + return self.get('LuckyNumbers')['LuckyNumber']['LuckyNumber'] + + @staticmethod + def __is_future(day): + """ + + :type day: librus_tricks.classes.SynergiaTeacherFreeDays + :return: + """ + if day.ends >= datetime.now().date(): + return True + return False + + def teacher_free_days(self, *days_ids, only_future=True): + """ + Zwraca dane przedmioty. + + :param int days_ids: Id zwolnień + :rtype: tuple[librus_tricks.classes.SynergiaTeacherFreeDays] + """ + if days_ids.__len__() == 0: + days = self.return_objects('Calendars', 'TeacherFreeDays', cls=SynergiaTeacherFreeDays) + else: + ids_computed = self.assembly_path(*days_ids, sep=',', suffix=',')[1:] + days = self.return_objects('Calendars', 'TeacherFreeDays', ids_computed, cls=SynergiaTeacherFreeDays) + + days = tuple(sorted(days, key=lambda x: x.starts)) + if only_future: + return tuple(filter(self.__is_future, days)) + return days + + def school_free_days(self, *days_ids, only_future=True): + if days_ids.__len__() == 0: + days = self.return_objects('Calendars', 'SchoolFreeDays', cls=SynergiaSchoolFreeDays) + else: + ids_computed = self.assembly_path(*days_ids, sep=',', suffix=',')[1:] + days = self.return_objects('Calendars', 'SchoolFreeDays', ids_computed, cls=SynergiaSchoolFreeDays) + + days = tuple(sorted(days, key=lambda x: x.starts)) + if only_future: + return tuple(filter(self.__is_future, days)) + return days + + def realizations(self, *realizations_ids): + if realizations_ids.__len__() == 0: + return self.return_objects('Realizations', cls=SynergiaRealization, extraction_key='Realizations') + ids_computed = self.assembly_path(*realizations_ids, sep=',', suffix=',')[1:] + return self.return_objects('Realizations', ids_computed, cls=SynergiaRealization, extraction_key='Realizations') + + def substitutions(self): + pass + + def preload_cache(self): + self.cache.clear_objects() + + for thing in (*self.attendances(), *self.grades(), *self.subjects(), *self.school_free_days(only_future=False), + *self.teacher_free_days(only_future=False)): + self.cache.add_object(thing.uid, thing.__class__, thing.export_resource()) + + logging.info('Loaded %s objects into cache', self.cache.count_object()) diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..6506ed5 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,82 @@ +class LibrusTricksException(Exception): + pass + + +class LibrusTricksAuthException(LibrusTricksException): + pass + + +class LibrusTricksWrapperException(LibrusTricksAuthException): + pass + + +class LibrusLoginError(LibrusTricksAuthException): + pass + + +class SynergiaAPIEndpointNotFound(LibrusTricksWrapperException): + pass + + +class LibrusPortalInvalidPasswordError(LibrusTricksAuthException): + pass + + +class SynergiaAccessDenied(LibrusTricksWrapperException): + pass + + +class WrongHTTPMethod(Exception): + pass + + +class SynergiaAPIInvalidRequest(LibrusTricksWrapperException): + pass + + +class TokenExpired(LibrusTricksException): + pass + + +class SynergiaForbidden(LibrusTricksWrapperException): + pass + + +class InvalidCacheManager(LibrusTricksAuthException): + pass + + +class CaptchaRequired(LibrusTricksAuthException): + pass + + +class SynergiaServerError(LibrusTricksWrapperException): + pass + + +class SessionRequired(LibrusTricksWrapperException): + pass + + +class APIPathIsEmpty(LibrusTricksWrapperException): + pass + + +class OtherHTTPResponse(LibrusTricksWrapperException): + pass + + +class SecurityWarning(Warning): + pass + + +class PerformanceWarning(Warning): + pass + + +class GoodPracticeWarning(Warning): + pass + + +class SynergiaMaintenanceError(SynergiaServerError): + pass diff --git a/messages.py b/messages.py new file mode 100644 index 0000000..e0bfe8c --- /dev/null +++ b/messages.py @@ -0,0 +1,82 @@ +from datetime import datetime +import logging + +import requests +from bs4 import BeautifulSoup + + +class SynergiaScrappedMessage: + def __init__(self, url, parent_web_session, header, author, message_date, synergia_session): + """ + :type url: str + :type parent_web_session: requests.sessions.Session + :type message_date: datetime + :type synergia_session: librus_tricks.core.SynergiaClient + """ + self.web_session = parent_web_session + self.url = url + self.header = header + self.author_alias = author + self.msg_date = message_date + self.synergia_session = synergia_session + + def __read_from_server(self): + response = self.web_session.get('https://synergia.librus.pl' + self.url) + soup = BeautifulSoup(response.text, 'html.parser') + return soup.find('div', attrs={'class': 'container-message-content'}).text + + @property + def text(self): + return self.__read_from_server() + + @property + def author(self): + from librus_tricks.classes import SynergiaTeacher + teachers = self.synergia_session.return_objects('Users', cls=SynergiaTeacher, extraction_key='Users') + for teacher in teachers: + if str(teacher.name) in self.author_alias and str(teacher.last_name) in self.author_alias: + return teacher + return + + def __repr__(self): + return f'' + + +class MessageReader: + def __init__(self, session): + """ + + :param librus_tricks.core.SynergiaClient session: + """ + self._syn_session = session + self._web_session = requests.session() + logging.debug('Obtain AutoLoginToken from server') + token = session.post('AutoLoginToken')['Token'] + self._web_session.get(f'https://synergia.librus.pl/loguj/token/{token}/przenies/wiadomosci') + logging.debug('Webscrapper logged into Synergia') + + def read_messages(self): + response = self._web_session.get('https://synergia.librus.pl/wiadomosci') + soup = BeautifulSoup(response.text, 'html.parser') + table = soup.find('table', attrs={'class': 'decorated stretch'}) + tbody = table.find('tbody') + + if 'Brak wiadomości' in tbody.text: + return None + + rows = tbody.find_all('tr') + messages = [] + for message in rows: + cols = message.find_all('td') + messages.append(SynergiaScrappedMessage( + url=cols[3].a['href'], + header=cols[3].text.strip(), + author=cols[2].text.strip(), + parent_web_session=self._web_session, + message_date=datetime.strptime(cols[4].text, '%Y-%m-%d %H:%M:%S'), + synergia_session=self._syn_session + )) + return messages + + def __repr__(self): + return f'<{self.__class__.__name__} for {self._syn_session.user}>' diff --git a/tools.py b/tools.py new file mode 100644 index 0000000..9dc7aef --- /dev/null +++ b/tools.py @@ -0,0 +1,185 @@ +from datetime import datetime, timedelta +import re + + +def get_next_monday(now=datetime.now()): + for _ in range(8): + if now.weekday() == 0: + return now.date() + now = now + timedelta(days=1) + return + + +def get_actual_monday(now=datetime.now()): + for _ in range(8): + if now.weekday() == 0: + return now.date() + now = now - timedelta(days=1) + return + + +def extract_percentage(grade): + """ + + :param librus_tricks.classes.SynergiaGrade grade: + :return: + """ + for comment in grade.comments: + matches = re.findall(r'(\d+)%', comment.text) + if matches.__len__() > 0: + return float(matches[0]) + return + + +def weighted_average(*grades_and_weights): + values = 0 + count = 0 + for grade_weight in grades_and_weights: + count += grade_weight[1] + for _ in range(grade_weight[1]): + values += grade_weight[0] + if count == 0: + return 0 + return values / count + + +def extracted_percentages(grades): + grades = [grade for grade in grades if extract_percentage(grade) is not None] + subjects = set([grade.subject.name for grade in grades]) + categorized = {} + for subject in subjects: + categorized[subject] = [] + for grade in grades: + categorized[grade.subject.name].append((grade, extract_percentage(grade))) + return categorized + + +def no_cache(func): + def wrapper(*args, **kwargs): + return func(*args, **{**kwargs, 'expire': 0}) + + return wrapper + + +def percentage_average(grades, generic_top_value=5): + def compare_lists(list_a, list_b): + result = [] + for item in list_a: + if item not in list_b: + result.append(item) + return result + + percentages = extracted_percentages(grades) + if percentages.keys().__len__() == 0: + return {} + generics = compare_lists(grades, [grade_weight_tuple[0] for grade_weight_tuple in tuple(percentages.values())[0]]) + averages = {} + for subject_name in percentages: + averages[subject_name] = weighted_average( + *[(grade_percent[1], grade_percent[0].category.weight) for grade_percent in percentages[subject_name]], + *[((generic.real_value / generic_top_value) * 100, generic.category.weight) for generic in generics if + generic_top_value is not None or not False if generic.real_value is not None if + generic.subject.name == subject_name] + ) + return averages + + +def subjects_averages(subject_keyed_grades): + averages = {} + for subject in subject_keyed_grades: + averages[subject] = weighted_average( + *[(grade.real_value, grade.category.weight) for grade in subject_keyed_grades[subject] if + grade.real_value is not None] + ) + + return averages + + +def count_attendances(attendances): + """ + + :param iterable[librus_tricks.classes.SynergiaAttendance] attendances: + :return: + """ + categories = set() + for attendance in attendances: + categories.add(attendance.type) + + results = {} + for cat in categories: + results[cat] = 0 + + for attendance in attendances: + results[attendance.type] += 1 + + return results + + +def present_percentage(attendances): + """ + + :param list[librus_tricks.classes.SynergiaAttendance] attendances: + :return: + """ + present = 0 + absent = 0 + for attendance in attendances: + if attendance.type.is_presence_kind: + present += 1 + else: + absent += 1 + + return present / attendances.__len__() * 100 + + +def percentages_of_attendances(attendances): + """ + + :param list[librus_tricks.classes.SynergiaAttendance] attendances: + :return: + """ + results = count_attendances(attendances) + for category in results: + results[category] = results[category] / attendances.__len__() * 100 + + return results + + +def attendance_per_subject(attendances): + """ + :type attendances: list of librus_tricks.classes.SynergiaAttendance + """ + subjects = set() + att_types = set() + + for att in attendances: + subjects.add( + att.lesson.subject + ) + att_types.add( + att.type + ) + + attendances_by_subject = dict() + + for sub in subjects: + attendances_by_subject[sub] = dict() + for attyp in att_types: + attendances_by_subject[sub][attyp] = list() + + for att in attendances: + attendances_by_subject[att.lesson.subject][att.type].append( + att + ) + + redundant = [] + + for subject in attendances_by_subject: + for at_type in attendances_by_subject[subject]: + if attendances_by_subject[subject][at_type].__len__() == 0: + redundant.append((subject, at_type)) + + for n in redundant: + del(attendances_by_subject[n[0]][n[1]]) + + return attendances_by_subject