PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB`Mini Shell

HOME


Mini Shell 1.0
DIR:/proc/self/root/opt/cloudlinux/venv/lib/python3.11/site-packages/xray/apiclient/
Upload File :
Current File : //proc/self/root/opt/cloudlinux/venv/lib/python3.11/site-packages/xray/apiclient/api_client.py
# -*- coding: utf-8 -*-

# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENSE.TXT

"""
This module contains class implementing MongoDB API interaction
"""
import dbm
import hashlib
import pwd
import json
import logging
import urllib.parse
import uuid
from functools import partial
from typing import List, Any, Iterable

from requests import Session, Response
from requests.adapters import HTTPAdapter
from requests.exceptions import RequestException
from requests.packages.urllib3.util.retry import Retry
from schema import Schema, SchemaError
from clwpos.papi import (
    is_feature_visible,
    is_feature_hidden_server_wide,
)

from ..adviser.advice_types import supported as supported_advice_types, get_advice_instance
from xray import gettext as _
from xray.apiclient.schemas import (
    detailed_advice_schema,
    user_sites_info_schema,
    advice_list_schema
)
from clcommon.clwpos_lib import is_wp_path
from clcommon.cpapi import docroot
from ..internal.constants import api_server, proto, adviser_api_server
from ..internal.exceptions import XRayAPIError, XRayAPIEmptyResponse, TaskNotFoundError
from ..internal.local_counters import open_local_storage
from ..internal.types import Task
from ..internal.user_plugin_utils import (
    get_xray_exec_user,
    user_mode_verification
)
from ..internal.utils import read_jwt_token
from xray.adviser.advice_helpers import filter_by_non_existence


class Client:
    """
    Base client class
    """

    def __init__(self, *, system_id: str,
                 tracing_task_id: str = 'unavailable'):
        self.system_id = system_id
        self.task_id = tracing_task_id
        self.logger = logging.getLogger('api_client')

        retry_conf = Retry(total=3,
                           allowed_methods=frozenset(['GET', 'POST']),
                           status_forcelist=frozenset([502, 503, 504]),
                           backoff_factor=3)  # sleeps 0s, 6s, 18s
        adapter = HTTPAdapter(max_retries=retry_conf)
        self.session = Session()
        self.session.mount(f'{proto}://', adapter)
        self.session.request = partial(self.session.request, timeout=10)

    def __repr__(self):
        return f'{self.__class__.__name__}::{self.main_endpoint}::tracing_task_id={self.task_id}'

    def __str__(self):
        return f'{self.__class__.__name__}::{self.task_id}'

    @property
    def main_endpoint(self) -> str:
        """
        Base endpoint
        """
        raise NotImplementedError(_('instances are to set their endpoint!'))

    @property
    def _headers(self) -> dict:
        """
        Updated request headers
        :return: dict with headers
        """
        return {
            'X-Auth': read_jwt_token()
        }

    def _query_string(self, **kwargs) -> str:
        """
        Construct query string
        :return: string including system_id and given kwargs
        """
        initial = {'system_id': self.system_id}
        if kwargs:
            initial.update(kwargs)
        return urllib.parse.urlencode(initial)

    def _preprocess_response(self, response: Response,
                             with_status_check: bool = True) -> Any:
        """
        Perform preprocessing checks of received Response:
        - is it OK, e.g. 200 OK
        - was it successful, e.g. status == ok
        and extract JSON from the Response object
        :return: full JSON representation of response
        """
        if not response.ok:
            # to not duplicate in logs
            self.logger.debug('Server responded: %s', response.text)
            request_data = f'{response.status_code}:{response.reason}'
            raise XRayAPIError(
                _('Unable to connect to server with %s') % request_data,
                extra={'resp_data': response.text})
        else:
            self.logger.info('[%s:%s] Response received %s',
                             response.status_code, response.reason,
                             response.url)

        # sometimes our services could return 204 code, which returns NO_CONTENT => json() fails
        if response.status_code != 204:
            result = response.json()
        else:
            result = {'status': 'ok'}
        if with_status_check:
            if result['status'] != 'ok':
                raise XRayAPIError(_("Received unsuccessful response: %s") % str(result))
        return result

    def _process_response(self, response: Response,
                          with_status_check: bool = True) -> dict:
        """
        Check received response
        :param response: a requests.Response object
        :return: 'result' dict from JSON representation of response
        """
        result = self._preprocess_response(response,
                                           with_status_check=with_status_check)
        try:
            data = result['result']
            self.logger.info('[%s] Received response data %s',
                             response.status_code, data)
            if data is None:
                raise XRayAPIEmptyResponse(result=result)
            return data
        except KeyError:
            return dict()

    def _post(self, endpoint: str, payload: dict = None, log_data: bool = True,
              with_status_check: bool = True, include_jwt: bool = True) -> dict:
        """
        Perform POST request to given endpoint.
        Add payload as JSON if given
        :param endpoint: target URL
        :param payload: dict with date to POST
        :param log_data: whether to log POST data or not
        :return: 'result' dict from JSON representation of response
        """
        self.logger.info('Sending POST request to %s',
                         endpoint)
        if log_data and payload:
            self.logger.info('Data attached to POST: %s', payload)
        try:
            if include_jwt:
                headers = self._headers
            else:
                headers = {}
            if payload is None:
                resp = self.session.post(endpoint, headers=headers)
            else:
                resp = self.session.post(endpoint,
                                         json=payload, headers=headers, timeout=60)
        except RequestException as e:
            raise self._give_xray_exception(e, endpoint, 'POST failed',
                                            _('Failed to POST data to X-Ray API server')) from e
        return self._process_response(resp,
                                      with_status_check=with_status_check)

    def _delete(self, endpoint: str, log_data: bool = True,
                with_status_check: bool = True):
        self.logger.info('Sending DELETE request to %s',
                         endpoint)
        try:
            resp = self.session.delete(endpoint, headers=self._headers)
        except RequestException as e:
            raise self._give_xray_exception(e, endpoint, 'DELETE failed',
                                            _('Failed to DELETE data to X-Ray API server')) from e
        return self._process_response(resp,
                                      with_status_check=with_status_check)

    def _raw_get(self, endpoint: str = None) -> Response:
        """
        GET request to endpoint or to main endpoint if no endpoint given
        :param endpoint: target URL
        :return: a requests Response object
        """
        if endpoint is None:
            endpoint = f'{self.main_endpoint}?{self._query_string()}'
        self.logger.info('Sending GET request to %s', endpoint)
        try:
            resp = self.session.get(endpoint, headers=self._headers)
        except RequestException as e:
            raise self._give_xray_exception(e, endpoint, 'GET failed',
                                            _('Failed to GET data from X-Ray API server')) from e
        return resp

    def _get_full(self, endpoint: str = None,
                  with_status_check: bool = True) -> Any:
        """
        GET request to endpoint or to main endpoint if no endpoint given
        :param endpoint: target URL
        :return: full dict from JSON representation of response without any
                 processing
        """
        resp = self._raw_get(endpoint)
        return self._preprocess_response(resp,
                                         with_status_check=with_status_check)

    def _get(self, endpoint: str = None) -> dict:
        """
        GET request to endpoint or to main endpoint if no endpoint given
        :param endpoint: target URL
        :return: 'result' dict from JSON representation of response
        """
        resp = self._raw_get(endpoint)
        try:
            return self._process_response(resp)
        except XRayAPIEmptyResponse as e:
            raise TaskNotFoundError(
                task_id=self.task_id
            ) from e

    def _give_xray_exception(self, exc, api_endpoint, log_message,
                             exc_message):
        """
        Process received exception
        :param exc: original exception
        :param api_endpoint: requested endpoint
        :param log_message: text for logging the error
        :param exc_message: text for internal exception
        """
        self.logger.error('%s with %s', log_message, exc,
                          extra={'endpoint': api_endpoint})
        try:
            exc_info = exc.args[0].reason
        except (IndexError, AttributeError):
            exc_info = exc

        exception_data = f'{exc_message}: {exc_info}'
        return XRayAPIError(
            _('%s. Please, try again later') % exception_data)


class TaskMixin:
    """
    A mixin class with Task related methods
    """

    @property
    def task_fields(self) -> tuple:
        """
        Limit processed fields
        """
        return ("url", "status", "client_ip", "tracing_by", "tracing_count",
                "starttime", "ini_location", "initial_count", "request_count",
                "auto_task", "user")

    def _task(self, dict_view: dict) -> Task:
        """
        Turn dictionary structure into valid Task type
        """
        task_view = {k: v for k, v in dict_view.items() if
                     k in self.task_fields}
        task_view['task_id'] = dict_view['tracing_task_id']
        return Task(**task_view)


class TasksClient(Client, TaskMixin):
    """
    'tasks' endpoint client
    """

    @property
    def main_endpoint(self) -> str:
        """
        Base endpoint: tasks
        """
        return f'{proto}://{api_server}/api/xray/tasks'

    def _query_string(self, **kwargs) -> str:
        """
        Construct query string.
        Aimed to get auto tasks only
        :return: string including system_id and type=auto
        """
        return super()._query_string(type='auto')

    def get_tasks(self) -> List[Task]:
        """
        Get list of Tasks
        """
        data = super()._get()
        return [self._task(item) for item in data]


class DBMClient(Client):
    """
    Client class using local dbm storage instead of remote API
    """

    def __init__(self, *, system_id: str,
                 tracing_task_id: str = 'unavailable'):
        super().__init__(system_id=system_id, tracing_task_id=tracing_task_id)
        self.task_object = None
        self._db = self._db_open()

    def __del__(self):
        self._db.close()

    @staticmethod
    def _db_open() -> 'gdbm object':
        """
        Open dbm DB
        :return: corresponding object
        """
        return dbm.open('/root/local_mongo', 'c')

    def _post(self, post_data: dict) -> None:
        """
        Update a DBM task with given data
        """
        self._db[self.task_id] = json.dumps(post_data)

    def _get(self) -> dict:
        """
        Get saved DBM data
        :return: dict
        """
        try:
            return json.loads(self._db[self.task_id].decode())
        except (KeyError, json.JSONDecodeError) as e:
            raise XRayAPIError(_('Failed to load task')) from e

    @staticmethod
    def _id() -> str:
        return uuid.uuid1().hex

    def get_task(self) -> Task:
        """
        Get saved task
        :return:
        """
        saved_task = self._get()
        saved_task['task_id'] = self.task_id
        self.task_object = Task(**saved_task)
        return self.task_object

    def create(self, task: Task) -> str:
        """
        Create new task and get unique ID
            url --> URL
            client_ip --> IP
            tracing_by --> time|request_qty
            tracing_count --> COUNT
            ini_location --> PATH
            status --> processing
        :param task: a Task instance
        :return: task ID
        """
        self.task_id = self._id()
        task.task_id = self.task_id
        task.status = 'hold'
        self._post(task.as_dict())
        self.task_object = task
        return self.task_id

    def update(self, starttime: int) -> None:
        """
        Update started|continued task
            status --> running
            starttime --> new timestamp
        :return:
        """
        if self.task_object is None:
            self.get_task()
        self.task_object.status = 'running'
        self.task_object.starttime = starttime
        self._post(self.task_object.as_dict())

    def stop(self, count: int) -> None:
        """
        Update stopped task
            status --> stopped
            tracing_count --> new value
        :return:
        """
        if self.task_object is None:
            self.get_task()
        self.task_object.status = 'stopped'
        self.task_object.tracing_count = count
        self._post(self.task_object.as_dict())

    def complete(self) -> None:
        """
        Complete tracing task
            status --> completed
        :return:
        """
        if self.task_object is None:
            self.get_task()
        self.task_object.status = 'completed'
        self._post(self.task_object.as_dict())

    def delete(self) -> None:
        """
        Delete tracing task
        :return:
        """
        del self._db[self.task_id]


class APIClient(Client, TaskMixin):
    """
    X-Ray task API client class
    """

    @property
    def main_endpoint(self) -> str:
        """
        Base endpoint: task
        """
        return f'{proto}://{api_server}/api/xray/task'

    def _query_string(self, **kwargs) -> str:
        """
        Construct query string
        :return: string either including system_id and task_id
                or system_id only
        """
        if self.task_id != 'unavailable':
            return super()._query_string(tracing_task_id=self.task_id)
        return super()._query_string()

    def _post_create(self, post_data: dict) -> None:
        """
        POST request to "create a task" API endpoint with given data
        Obtains a task ID
        :param post_data: dict with POST data
        """
        endpoint = f'{self.main_endpoint}/create?{self._query_string()}'
        response_data = self._post(endpoint,
                                   {k: v for k, v in post_data.items() if
                                    k != 'starttime'})
        self.task_id = response_data['tracing_task_id']

    def _post_update(self, post_data: dict) -> None:
        """
        POST request to "update a task" API endpoint with given data
        :param post_data: dict with POST data
        """
        endpoint = f'{self.main_endpoint}/update?{self._query_string()}'
        self._post(endpoint, post_data)

    def _share(self) -> None:
        """
        GET request to "share a task" API endpoint
        """
        share_endpoint = self.main_endpoint[:-5]
        endpoint = f'{share_endpoint}/share-request?{self._query_string()}'
        self._get(endpoint)

    def _delete(self) -> None:
        """
        POST request to "delete a task" API endpoint
        """
        endpoint = f'{self.main_endpoint}/delete?{self._query_string()}'
        self._post(endpoint)

    @user_mode_verification
    def get_task(self) -> Task:
        """
        Get saved task
        :return:
        """
        return self._task(self._get())

    def create(self, task: Task) -> Task:
        """
        Create new task and get unique ID
            url --> URL
            client_ip --> IP
            tracing_by --> time|request_qty
            tracing_count --> COUNT
            ini_location --> PATH
            status --> processing
        :param task: a Task instance
        :return: updated Task instance
        """
        task.status = 'hold'
        self._post_create({k: v for k, v in task.as_dict().items() if
                           k in self.task_fields})
        return self.task_id

    def update(self, starttime: int) -> None:
        """
        Update started|continued task
            status --> running
            starttime --> new timestamp
        :param starttime: time of starting the Task
        """
        self._post_update({'status': 'running',
                           'starttime': starttime})

    def update_count_only(self, count: int) -> None:
        """
        Update tracing_count only. No status updated
            tracing_count --> new value
        :return:
        """
        self._post_update({'tracing_count': count})

    def update_counts_only(self, *, request_count: int,
                           tracing_count: int = None) -> None:
        """
        Update tracing_count only. No status updated
            request_count --> new value
            tracing_count --> new value if given
        :param request_count: number of requests already traced
        :param tracing_count: number of requests left to trace
        """
        if tracing_count is None:
            data = {'request_count': request_count}
        else:
            data = {'request_count': request_count,
                    'tracing_count': tracing_count}
        self._post_update(data)

    def stop(self, count: int) -> None:
        """
        Update stopped task
            status --> stopped
            tracing_count --> new value
        :return:
        """
        self._post_update({'status': 'stopped',
                           'tracing_count': count})

    def complete(self) -> None:
        """
        Complete tracing task
            status --> completed
        :return:
        """
        self._post_update({'status': 'completed'})

    def share(self) -> None:
        """
        Share tracing task
        :return:
        """
        self._share()

    def delete(self) -> None:
        """
        Delete tracing task
        :return:
        """
        self._delete()


class SendClient(Client):
    """
    X-Ray requests API client class
    """

    @property
    def main_endpoint(self) -> str:
        """
        Base endpoint: requests
        """
        return f'{proto}://{api_server}/api/xray/requests'

    def __call__(self, data: dict) -> None:
        """
        Send given data to ClickHouse
        :param data: dict with data
        """
        endpoint = f'{self.main_endpoint}?{self._query_string()}'
        self._post(endpoint, data, log_data=False)


class UIAPIClient(Client):
    """
    X-Ray User plugin API client class
    """

    @property
    def main_endpoint(self) -> str:
        """
        Base endpoint: requests
        """
        return f'{proto}://{api_server}/api/xray'

    def _query_string(self, **kwargs) -> str:
        """
        Construct query string
        :return: string including system_id
                 and given kwargs, filtered by non-empty values
        """
        filter_empty = {k: v for k, v in kwargs.items() if v is not None}
        return super()._query_string(**filter_empty)

    def get_task_list(self) -> dict:
        """
        Get list of tasks and return not processed (full) response
        from API server
        """
        qs = self._query_string(user=get_xray_exec_user())
        endpoint = f'{self.main_endpoint}/tasks?{qs}'
        response = self._get_full(endpoint)
        for task in response['result']:
            # mix up local data for all tasks except completed
            # completed tasks have all the data actual in mongo
            if task['status'] == 'completed':
                continue

            fake_id = hashlib.blake2b(task['tracing_task_id'].encode(),
                                      digest_size=10).hexdigest()
            with open_local_storage(fake_id) as storage:
                if task['tracing_by'] != 'time':
                    task['tracing_count'] = task['initial_count'] - storage.processed_requests
                task['request_count'] = storage.processed_requests
        return response

    def get_request_list(self, task_id: str) -> dict:
        """
        Get list of requests collected for given tracing task
        """
        qs = self._query_string(tracing_task_id=task_id)
        endpoint = f'{self.main_endpoint}/requests?{qs}'
        return self._get_full(endpoint)

    def get_request_data(self, task_id: str, request_id: int) -> dict:
        """
        Get collected statistics for given request ID of given tracing task
        """
        qs = self._query_string(tracing_task_id=task_id,
                                request_id=request_id)
        endpoint = f'{self.main_endpoint}/request?{qs}'
        return self._get_full(endpoint)


class SmartAdviceAPIClient(Client):
    """
    X-Ray Adviser API client class
    """

    def __init__(self):
        super().__init__(system_id='not_needed')

    def _validate(self, data: Any, schema: Schema) -> Any:
        """Validate given data using given schema"""
        try:
            return schema.validate(data)
        except SchemaError as e:
            self.logger.error('Failed to validate API response: %s', data)
            msg = e.errors[-1] or e.autos[-1]
            raise XRayAPIError(_('Malformed API response: %s') % str(msg))


    @property
    def main_endpoint(self) -> str:
        """
        Base endpoint: requests
        """
        return f'https://{adviser_api_server}/api'

    @property
    def fields_allowed(self) -> tuple:
        """
        Limit fields available for update
        """
        return ("status", "source", "reason")

    def _query_string(self, **kwargs) -> str:
        """
        Construct query string
        :return: string including types and given kwargs
        """
        initial = [('type', _t) for _t in supported_advice_types]
        user_context = get_xray_exec_user()
        if user_context:
            initial.append(('username', user_context))
        initial.extend([(k, v) for k, v in kwargs.items() if v])
        return urllib.parse.urlencode(initial, safe=',')

    def __call__(self, data: dict) -> None:
        """
        Send given data to Adviser microservice
        :param data: dict with data
        """
        endpoint = f'{self.main_endpoint}/requests/add'
        self._post(endpoint, data, log_data=False,
                   with_status_check=False)

    def _patch(self, endpoint: str, payload: dict = None) -> Any:
        """
        Perform PATCH request to given endpoint.
        Add payload as JSON.
        :param endpoint: target URL
        :param payload: dict with data to PATCH
        :return: full response
        """
        self.logger.info('Sending PATCH request to %s',
                         endpoint)

        try:
            resp = self.session.patch(endpoint,
                                      json=payload,
                                      headers=self._headers)
        except RequestException as e:
            raise self._give_xray_exception(e, endpoint, 'PATCH failed',
                                            _('Failed to PATCH data to Smart Advice API server')) from e

        return self._preprocess_response(resp, with_status_check=False)

    def send_stat(self, data: dict) -> None:
        """
        Send statistics to Adviser microservice
        """
        endpoint = f'{self.main_endpoint}/requests/metadata'
        self._post(endpoint, data, with_status_check=False)

    def _filter_advice_list(self, advice_list: List[dict]):
        """
        Loop over advices and remove those which have
        non-existing users and those which are invisible.
        :param advice_list: list of advices received from API
        """

        visible_advices = []
        filtered = filter_by_non_existence(advice_list)
        for item in filtered:
            advice_instance = get_advice_instance(item['advice']['type'])
            if is_feature_visible(advice_instance.module_name,
                                  item['metadata']['username']) and \
                    not is_feature_hidden_server_wide(advice_instance.module_name):
                visible_advices.append(item)
        return visible_advices

    def advice_list(self, filtered: bool = True, show_all: bool = False) -> List:
        """
        Get list of advice
        :param filtered: Automatically removes invisible advices
                         and those which are inked to non-existing users.
        """
        endpoint = f'{self.main_endpoint}/advice/list?{self._query_string(show_all=show_all)}'
        response = self._get_full(endpoint, with_status_check=False)

        response = self._validate(
            data=response,
            schema=advice_list_schema)

        if filtered:
            response = self._filter_advice_list(response)
        return response

    def site_info(self, username) -> List:
        """
        Get urls/advices information per user`s site
        """
        endpoint = f'{self.main_endpoint}/advice/site_info/{username}'

        response = self._get_full(endpoint, with_status_check=False)
        return self._validate(
            data=response,
            schema=user_sites_info_schema)

    def advice_details(self, advice_id: int) -> dict:
        """
        Get details of an advice by given advice_id
        """
        endpoint = f'{self.main_endpoint}/v2/advice/{advice_id}/details'

        response = self._get_full(endpoint, with_status_check=False)
        return self._validate(
            data=response,
            schema=detailed_advice_schema)

    def update_advice(self, advice_id: int, **kwargs) -> Any:
        """
        Partial update of an advice by given advice_id.
        Fields allowed for update are limited by fields_allowed property
        """
        data = {k: v for k, v in kwargs.items() if
                k in self.fields_allowed}
        endpoint = f'{self.main_endpoint}/advice/{advice_id}'
        return self._patch(endpoint, data)

    def report(self, data: dict) -> Any:
        """
        Sends analytics data to the microservice
        """
        endpoint = f'{self.main_endpoint}/analytics/events'
        return self._post(endpoint, data, with_status_check=False, include_jwt=False)


class AWPProvisionAPIClient(Client):
    """
    X-Ray Adviser API client class
    """

    def __init__(self):
        super().__init__(system_id='not_needed')

    def _query_string(self, **kwargs) -> str:
        return urllib.parse.urlencode(kwargs)

    @property
    def main_endpoint(self) -> str:
        """
        Base endpoint: requests
        """
        return f'https://{adviser_api_server}/awp'

    def _process_response(self, response: Response,
                          with_status_check: bool = True) -> dict:
        return self._preprocess_response(response,
                                         with_status_check=with_status_check)

    def get_create_pullzone(self, account_id: str, domain: str, website: str):
        """
        Gets pullzone if already exists, otherwise creates it
        """
        endpoint = f'{self.main_endpoint}/cdn/pullzone'
        return self._post(endpoint, {'account_id': account_id,
                                     'original_url': domain,
                                     'website': website},
                          log_data=False,
                   with_status_check=False)

    def remove_pullzone(self, account_id: str, domain: str, website: str):
        """
        Gets pullzone if already exists, otherwise creates it
        """
        endpoint = f'{self.main_endpoint}/cdn/pullzone'
        return self._delete(f'{endpoint}?{self._query_string(account_id=account_id, original_url=domain, website=website)}',
                            log_data=False,
                            with_status_check=False)

    def purge_cdn_cache(self, account_id: str, domain: str, website: str):
        endpoint = f'{self.main_endpoint}/cdn/purge'
        return self._post(endpoint, {'account_id': account_id,
                                     'original_url': domain,
                                     'website': website},
                          log_data=False,
                          with_status_check=False)

    def sync_account(self, account_id: Iterable[str]):
        endpoint = f'{self.main_endpoint}/public/account/sync'
        return self._post(endpoint, {'account_id': account_id},
                          log_data=False,
                          with_status_check=False)

    def get_usage(self, account_id: str):
        endpoint = f'{self.main_endpoint}/cdn/usage?{self._query_string(account_id=account_id)}'
        return self._get(endpoint)