From b632098d64ad3a18d3dd0fd9fe445483f24eb9a6 Mon Sep 17 00:00:00 2001 From: jax Date: Mon, 25 Mar 2019 15:27:26 +0000 Subject: [PATCH] Initializing OSS repo with copy over from personal repo --- CODE_OF_CONDUCT.md | 74 +++++++ LICENSE.txt | 20 ++ doc/logo/logo.png | Bin 0 -> 139177 bytes henry/.support_files/help.rtf | 43 ++++ henry/.support_files/logging.conf | 72 ++++++ henry/__init__.py | 0 henry/__version__.py | 1 + henry/cli.py | 319 +++++++++++++++++++++++++++ henry/commands/__init__.py | 0 henry/commands/analyze.py | 168 ++++++++++++++ henry/commands/pulse.py | 353 ++++++++++++++++++++++++++++++ henry/commands/vacuum.py | 124 +++++++++++ henry/modules/__init__.py | 0 henry/modules/auth.py | 81 +++++++ henry/modules/color.py | 32 +++ henry/modules/data_controller.py | 52 +++++ henry/modules/fetcher.py | 260 ++++++++++++++++++++++ henry/modules/lookerapi.py | 343 +++++++++++++++++++++++++++++ henry/modules/spinner.py | 32 +++ readme.md | 296 +++++++++++++++++++++++++ setup.py | 117 ++++++++++ 21 files changed, 2387 insertions(+) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE.txt create mode 100644 doc/logo/logo.png create mode 100644 henry/.support_files/help.rtf create mode 100644 henry/.support_files/logging.conf create mode 100644 henry/__init__.py create mode 100644 henry/__version__.py create mode 100755 henry/cli.py create mode 100644 henry/commands/__init__.py create mode 100644 henry/commands/analyze.py create mode 100644 henry/commands/pulse.py create mode 100644 henry/commands/vacuum.py create mode 100644 henry/modules/__init__.py create mode 100644 henry/modules/auth.py create mode 100644 henry/modules/color.py create mode 100644 henry/modules/data_controller.py create mode 100644 henry/modules/fetcher.py create mode 100644 henry/modules/lookerapi.py create mode 100644 henry/modules/spinner.py create mode 100644 readme.md create mode 100644 setup.py diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7bc8a29 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at jax@looker.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..cc94518 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 Joseph Axisa Looker Data Sciences, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/logo/logo.png b/doc/logo/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..56fb38e3fe44ed9dc6496f68996de485fc3b9c5d GIT binary patch literal 139177 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 AsLB%+P8mq0Qm<;{1fh=U=WP# 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

LT5`Y3O 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�-prZzD+iq{NtO7!~L1^r@R* zUb&V+hDu-~WVm!euTR&Fjf06cxT?<-P2|lG@_eDBOCyDQ&LRMfYD4ff!kmm*vYoATnpVR2eA z7HbSTTiJq_D2mXg<=kPqN8r4g=4j`GZWo@1a10s(pCfltj}s5w;4nS1%k^y6ioOxd zI007@{;z!1D14Q0k=3=`lrE&~3>^oJNQUS5HXMHcH{AFLz}^|-Pzb{?R27K(pOB;Q z!G%}c5Lj6LKqUkdf~rkY$8kOf@{P*45_Gj~*f}P8wUA}-P{Z|&Tn)oGJ+?&kQ%nY> zL|UpgGrcA>BpZ=MYi$7^j3gWk(LGEhMT)vBvX|p=D(x_qR!k?O#YxBEBkD-VLpW4K z9Z6<1vW}UVP$W7{e&K+DxS~_X;8dZ=-+l_hxst;GV&Y_gA@3Pq%TWWdbjkAwCB`m) zU4(Dl`ZAW5-j4&v$vA|z3pJJolX0O(Wx|4&;K0QS@8&-fnmh|Ox96cW)dbju{_oPc z(%dQ6B@%|dO+g8@NpDBYNkzhm?W5@*ZO1AAduOx4AP@s#*Phx_-~TBKA_xktU+Sze zrqbRD?SeNE70v8Se&!D#KfdVoP{ozk*7ZU2GeOyvbd-i%rG(P#B!nnOz2mhVdk}Ok z64;UD>bL|H0bK+e8=3yX%?}9+HpMPdG7^D7IzK^%M#Uq?!mjHpxR|829NOjkM!0N8 z>Zk2y10A^*_pLE8zPilvGX=?Kn3>^i1N=U)ioq#3`w{6TF`k+VO6Lm_+I2H-J!3Fp zChfgVtI*c`K-o2Nt~o9hSWNbu%Ex&22k`3|B#-)D8UbzQS{mPXmSi8NXEUbQ6H7ie zjGUpY80%UX3oIP4<{fIbRxIKDbVWOfX#A!7{MKHOhXCxI%?^S<41@(ujES0f-1mPe zPriZ&G%?{P@-+-}%ig?rfD3ye0T$X$r|oPGWFyT%ACK4oC_^h%INw+$y=!wYUAy{0 z{W?x2jmA{YNYp z6;(o5{+;MMQ2Vf;gs*O>>Msw`Z&z{E&& zSd7uO)4qOP9LVB44*HL4(=EhH>)bpYh^)`QPhF&8m@AT7Y>73dYg6N}*Ec1j)CF@I zK;qzSbkF-k-Wdir+*7*HP{_tY$Yk+)yM|N7>3AMLhXDq9d~W0{koHtN0 ziqL6Lsr;gf&8n=zo(6+jOnHB(oVvjgW{+O_S+FcPMA_)Dz8sle&TS1R>mK2PsSv=X zgZQjARbssLOn1Bwr%&^}d({~!tI>kh7 z(9-N-|N96t{VOW(O91xHXa!*yhN7bQsQ!cO|37T{35w%%JCc<|Uwja>P&PJbyLD@t zo8;y%Kz_6=!sAj24~svo(1(8VoHrl%Z4ME5G_tpXuwL!HGXrG5LzQ~FU)Y3(3_{Ex z4kMJ`(eYOp|K0C@6C5~>*VaFphrD8FiO0j~>C}KWA_MRz$~;yxDehJfV}P)X z1^FrgG6}%m6|7JQ!!Xf#74iRnRKY3P%!qA|9t0J{+f($dS(7&X0px=RDy~ph&Rv}H z)i&6YI>3U%BIBaMwbardITX#Kj3ELM*Zk&zF5I;>ibNlEm=HfX)nV_>>1E5-jQwyP1Q)#oQiNXXS+lSMv&n1iD zcipzt^;YaVf>i}d0GFI3!vjJdj-=GK^{2tElqkj>&l=_F}x(v!af`LPR0bCm0@pQG2L zqdznaJgb)|Pw@0GBap~Qw#GfEuob~r38mHFo(3*LK(Gs-&w;$oBddEb?31^13|^pBtCyCUTE?8b82bd) zqgm~GEvQhebBdFZlQzkhzkvMS%7cUEDLvNWsBLPYwhzS(3~ySuPjD{0UDSc3mD1MZ zEcK+f20kY^IRl!$A3iz~!nP>D&u5!fY4O<-|Di%9u>>9Rx{AW#&5A|lqg#&wrQ12V z4wxzaK8-c#3T3TMWo=7PAAl)hbX}j_LTo^v1C7eR-Bn`N;-mP;R|g`wl=!4yftCdyc2$@(wqqH7o);sZ1rF9*R)?>*vf3cts=*v^bV~Qt|F3i&o956!G(uiXv zP^Ikw$8WVk!TR=)vqW{8x+{WcVRvobI#wiWcBT;h6hC@{glI|E^SB-uO zz}^+@00aZEA^!hA?IXsCa=P{AQG!G~j74U-)AqLh3FHHZxhfj+usyESuCO7>CYLww zVQna+<$;0z{U5YCBLvbzFc68Od!F{MuIkyHS5HMI50xM*3Wov=#+koQgg$z0s)%9c z(qC|1$ITppLp4C!PC@}HZr`3+yfpuHR%CJ%30KLG*b+Y15Z;5Lw#OD z`idKDIx+9Wqxo*zTE^eTSR3@_c_}r35Qa;dglaG;Wl@`|NUqKrjq_5Q#+m|GzRr z+0nC_5>Fx|GK*!*rDt6)=Le9FZz)B|!ClwRm^@*IOtbvMVip;qrj_8?o%I5G0vw(O zphBT?m$2A|a#sVf=e;GcFvUyl<*wa=jp}Fb$?KzZy(<7=<9`YK7!Wvq1N)G^2b)Pi zy|9|SKWGgbOM})g}GsP ztPC{Js?Ic!5+G#li3%I?c^CH-K zA%upokQ)oI11DiU5OjNhVm}94IRg-Yy>nXu5Cno~6#f7A7G_~^YS`S>$B2R+f?oHJAqIQ55Xd4Spc*+KCwPs=>v*eZrrRb;jG~>Nev?x< zahR^qmeo*CDPPZ&Q)`(uDw7_S3T)1*XHrxrX^iDRCI6yinP|J~{OvkbBxZM4!BDC6V6R-mX7@eAGm}6YLjz zvc3Z#+l=ceD%GL{A4rXOpyDJ7e|`z8c?=*vxcKEH+oUgz=4I={G8cX40EE3a z^X&nU-OKKwZ@jbbYE^*0(zmhy5m-#Q3{KN{S;i|;YDbU^`yN#c>}dvzejC_vwVHUy zeQ1GAW^y<};D*!;WM(uAiit&T6DP-0CcywB2PRktCZlekTYF;$vI@Z78LS`-13^@f zfQT3M|9>VQ;~#jlSSI%Abj`-Ah=E*6TUtnVH@iDCe+Tk|1H1=E^`4>mw6Gnx1`b5v zDTJFu`%r7$9M7lK22_Nns81&r0Bb73V@S|fpO)?&@h+hdhryuM;(S0(y6DA4oFto$ zlnE&|FYXg%`?G+5+^Waiar^|Ed7#x?owN=Da3r7dGSA#=J_y znWj9cGuVPNpljJZ9JnY{Bv_@GEy1W)^_GGgx5|27+jc9{L^m z{}1vXq(a-PsGZ1!VM$^_FCqp!3W8>5v+KUO1>|B2kVMRT{65kl+4O6~FOW|}2SN5h zkhyv9-TV6qn}okZ>J&6HP_;!R=Ls8dpNxJ+qa-d)O+!TVL}Pm`8f++(xOcajouk2% zV(P7DIZ+qHTc}sLod&iUX$X*gR}>HpAgP9I(rUM%XLA(bYXl^B0;wq>KAihYK-Q%z zmI-sB8K>_<1Y3&2m+K?hXH9vx0N+z8k}O363=18KSc zyp@|xorgP|P1nHJT5a{-dt2RV(JevL=rzK!%0`dgt&JA;(IP_hUZQNYM2i-LAbRwO z9t2TC9(IfJpbs}y31>V zK0M^H8oFU4hX+By0yswPf#UV^1HtO`tAX!GjMI1S! zngVqYW{_xxR_gj^sGCpv5U4P33%x6)R=}xAa-|#+jLp)*Ftd04N=-RU}|n* zp|N|NadNXBYEy_UufYXexe*1iPoTMajtWxnq@5)IuZO+jM~}$-(^^nV;DyHAKa$G` zoHi}!&p3Qc%>9E36Cz%jDw%RK|HF$eTU{T|RB@(ciE92SF!8!%4P@T>Wh?K@rX;@b z=+#&Y1|K!tvT)26Ne-%^Gvh@yYyK)ka@7TxlA!cwU2FF{KGb~g+y|{an%%s0si*2u zD{+j3X9REtG3$%tx2RD3x*bw(2&d<6S{Pm|C6u!3p4|C+Dzmj@ij9FMeR}uvs@ml#=Tv1FN|HJF&5MUn9Q>jHQEp{k!&4XYAi84U!BDEp z@?iGb6|Dg-&ghGKAOk?W?TB1)RH@uM31zD9>pG8Y&g9?n6)UWZ8DSKA1|tn< zS6a%!*#cwI{$nOB>ZVp8k0HqMD{y8o#?FJwyEDqS7pEd_=i@;%%f)ejqi=pueSHRgvM|A@#Ty%=@XPo|Lsdhyw^O7<`r zMtz~R@RM#9EK#UYCy|n8)#RG#Gn@Za{u0VSdiu1jl2t*D{j7hVg8-jd?Rtad>N4Tc zmj7P8Z3O9_UC;N;og`Bp9#7V99kvc$rgz?;W0KUfq=wLPnh6UON&)l^dO^=l5xx+T z_}22uYiEa+eIwEJ^yQ~Tk5YMy&f7uR9dSVT*Lqyq3qKhyf}dk^dRK_Pru77Y40Le( zQNNc6+dOLSb}hXcifV`8iyQV&sVd`b4~S#tS^k`zvWaD-SgbZmX7Z1L_anL}VW;i# zvYK%q`TF0lN5UGFjR+%BT=@Az0~z14F{U))DS%PiQ%uxaIpkg=$j(cBW#JlmXr3tzwQuJ}8T@-Gy^(%Hx&sQ!vCg#6;Beh(AX zPduYT#@>sM$oMBb!=)lugjybcl-E~c_bjgBTtnucFrmcB@gv%wHcB{FeSM%)Ox*pJ zb#tlH;Y(QtH$y(Oa!ZUG`l)V^sPhXk^0dzA(ymaPb1~bDixf>Pfe0jSe%-1_tR{1E zAqf7gb3_i#Q5f-vWVS3hB0gWg-sHEz+oXI%{GA5D)`U9r|CY!_*(6z*HQyPYqV2=; z2gG+9k(@pQ%(L|_%KEwZ?OSd_rr+>@)r-SwZ8Sk493Q~7l#FSp#6R<{fC9lDZg4U# zSiV01V1V#l+<2|96Z|N*(8o%8;K`=)HUP|~Urt;l^Wg+Q{emI!7k!i^ps&=0E=Bus!X{O0IR&*z>}K5qxeJZIKw%zpXA& z7kEK%dTux@qwDQzP0H`1N^zya5&g98uY0lZpM2i$UeMWaAAQ)JM+oE}h=U7U6PaW* zI1w8gTbNbmqSU0+(Sdh0&7gsj-U?Z2}p=B*{xZ9LEePhUv6Vddu;c|7cgX_+|VEU#d9DNE%@fOr7<>C@&I6YdOBaPods zcZ81K&H8TA<*{L+Bm(o}fx(^TTSqMHA@MZKAjQc}tj-u&Fq<;JZ-P|z_MC}`Pyp+< zP1r^LOmY2sSo_4@Z?^>$<5^tO>Y8j%qnPEKU4S-)4V))eM6%I%g?FqDkr6%-D_r_q z?H)Bnbb>2tY5Gf0MEiov_n>|W%Xe&%!$M%E2#Cu}gZPtwhaVM>y(pofULGEQ=z0e@ z$?b6>+DWDPH1W~(ibNGwG1wL)QMv%8Dlgb}gEUUnNI7uRj(VcZVNGZN0j#VS0G`rU zAtG@0AkUIA;~MfcUWjd^+-4r!)F7@m>VJf%?OGZdsP+e0^fiQ1sMq?8O6vf5tRJ~JChxCw*J)|u?&Ly5_kFJ9dzFh1 zxk{iwa@9}kP!N|iaf^TCQRw5ns1W31JAaS!{G)FyM)v;6uHWF945bjnOrG}JOxN`JCfy6`qR+_*|l**{Sk)Y9{^u)N0}HDUIbQ|riM zQh>MJ*0Ms_7=eaaBd_%PWVWPqJ+`#TE{Eq6un(82fXCk#o{H%& z%QOg(NP_#az~0{)qG*Ha$g53%rLvi9>ZE^av%90#{Br)?gfyhdol^uw_AQY5x&+gI zu;;dt<#OIuQ)~Tq2Q{UNFNXhXDN0{Fz#2kX{OW>5gR$+|z9mc9uI6B8Y&O{2PA1LE z6w*07`ezixHGNI*Tms;YA%x1q#YaEe4K*KF|BQfW4U|EJ90#sRLKs%QG3{o1tnnby zih70lKBSc=vp?|D*SE-vflDOp|61zN$PR`bXHuYM`uazthJ5odgcZ|uN{pXUMJx_K z2bZt_K(KWP%ZT6A_UzCUWtZ6Vv8)`acY?>G+conHHLA0#AuKXT_c||5N_4pY6J-F2 zN*5k%3i3QRip^Q=t*epeGW+dkI(ZELx|)Jq1;;(1!CzxlIdmwhgQcMF^b$<|@2=8o z6!U_w?qsR_RB$rDJcWQ_xL6P;VhTi;-NS15D-tRw#rPr4=Xy@1aY1WN#GrVWpdm)s z>JEjEE6CcjH}a&9g$+s_=+<@D`kz|K>?$Ol{D zeOb#TyrH^O--hZUjtTi|-i$b1REh96rssE4_*!absaj*+BB7y>CJ_SS5OaoL9@g{w z?apD99?+IoV$s*rmu7k%D%Y;61tC^5&r^H%p8^ZVrh13UUDuFw&M?;0b*BRN)GaV7 zFO*SC;l{FLrLloLmO8YquaO?KvW?V#_$_SMxabOo)TW7^jEqBOoXmTY!tSRX~j!1K*5I1FjnB33YNgjZ$|9gMaKjUxm zLJJyunjTm%uGA=B?-)0a$%d;1h~ccRmkf@Q1?s@%*%)p=+{zyW4`oEgSKA zi1PE(SvFKd5#Pcc6n1#h(rhuZ7_NFDNC6JN3yCR0N2fihm?qA#<;~t*X5n|hQt$R+ z^xfDr65$E5RgY~vZYEM!mI7VqadA#vJStorXyTforjf}3cW2qFOlyw#r~t8MeKB{& zhuNci)6!`luHD^a8}eQ4bGb{0U*0Kv%Is@;%iP&5>Sl_HuIyMYDbuc=T}DQBf2H=b zxL&cAG>X$LO6JIU(-*${kw%{p1(*~7l)~QtFtZqun*~8CC*qs}?qihY^yO|l`a`hY zr1JNbV0+A^sQOy*AE4BpBdWyA&e+@g6zDRT$1KH$Yzb&}V{4ADi^Ebv2N923Pbu$4 z_)*&7T%~ZjiTC%ODJ#cYs$-vRJEde;3FCvMa*w&kHm$r@rm8M@h_gE(M_7PpV8crlhPdt_5Vww9Kc6GuZLJjcj z7aWbmst98{yx{b7*Kwwzq6TDtK^=2*b8Cj>dwZ|1rA!-N#p_)Y{&m%Gq*r`sK+>Vw zYo&RDvuet9lV62g&0I*MJtC*k9DPCj9IsZNc>@=zxUt%XxE!myx@%6ib-@d$lPJBN zX}qF4vyqvWIuE^`=xaf+yyqjv(!eA{HaH`c>hdmDV#Bt|#>{zEO3S7ZM}2-KaW}VE ze9I$nO8x0fN}n4r8W;8Q@xhSdPQNvnDtj(xhIS8%`nYa*^il(rrz_2#6t0$9XSEo0 z^SQ9#2)DY&*$H4w#3fy832Qe#ZVlE#NmA)V2(Cq8J_|azq)!xoa`J&(E-%kXik{d% z&!Lc7e&O($O(7&ncqlkzilaAO>6wbl)zMH;$3>Pk3=R31VA=O3-QU%^>;VLo>@iXq zhXlmUAT#dW~nz8E1GH6#Ict&)(J?SDb=Z7*Oa>P-DqlJ2 zhM&no%NSgo8eR^o47FQoe{_5NtpJD&AoifcpdFPx2}P?yCSOVQhe6*9GnjfS^O;8# zImpCOKxR|-Wspp;f~B4yNBUZ{dbbgw92tRCKo`?9EFzduEgE1vYvtL~7 zzUpt9ASS_DM#9%9qB;gM3kw%xGc(7DX=!A7db$npAV#^T@qzmU5CveVgK76dm>ge{ z+76%sKanl@GDCy&H=JmdKE^koxhrcm8sNTTP{%iLc&^~25gu$+5Pyjm$a;-Fw@W)J zi#a!d4|Vp$@4@an6(xb1DYb2F7tQVMZ%2*wjR1HuwlNsAi6q$f!YM7~&jZzdGF6FZ z`&a|JIJx3TXy))DOmht8xDbdZyM9f>0>nrQI6Gn94!ZzxWTO{46v~m2gg`Q50~1_l`1}{+`NDjd(H6iC8!d`+UFE; zK1RPGde#Jp8(%0YG$ZV8VsHOK*xE;0!4VWefee*Mn*&%vd=i`pfK;$j`cDuhf>PAB z3iH`?_M`~2wbuO+DOZgLlWY6sYf7Tjxytn%WUcHc^TI-B(pdQC&=_lydD>99%x#r4 z93J<7dxTcEn(UdM;Fdb`ANTbh{b;35)d4_F97a0>lz2(62DLVmKuZKj>?-k@jN20^ z^sU={*;N1#*$iYRz>SEkI*G6?L%jx$EkJ1RW;37V|DLah56*`7&hhpJ;Wnym{drkH zk9{IU#}h*FxcC1>fp&b{ThyTVOsbHF(+91po)XoqSlq^92e_K2YX)o5eYzFm<2wt5 zB;5P|zEax^!$?Eq$`o?G#!~cr4CbbKkOWfT zzj{(!NVR(X0MflyEv@>+f6^Y@SG! zR0nL5v;3L-#D572rxl-oIP#4Q3+V4u{?(FCI1=+yh`J ZJOHF8xIQy3O%{OrbhRF8)@#7g{{v*2o_qiR literal 0 HcmV?d00001 diff --git a/henry/.support_files/help.rtf b/henry/.support_files/help.rtf new file mode 100644 index 0000000..6a033fa --- /dev/null +++ b/henry/.support_files/help.rtf @@ -0,0 +1,43 @@ + ./#&@@@@@@@@&@@@@#/, + /%&@@#,,****,,.*@@ ((/( + ,&@@@@&%/*,,,,,*/(#%&&%%% + /&%...,,,***/////(##%&&&&@@@@@%# + *% ....,,**//((#%%&&&@@@@@@@@@&%# + (&@@@@@@@@@@@@@@@@@@@@@@@@@@@@&&&%. + #&@@&@@#%@@@&@@@@@@@@@@@@@@@@@&&&% + .#@@@%@@/%*/(@,@/@#*@@@@@@@@@@@&&&%. + ,#@@@@@@@@@@@@%#(%*@@@@@@@@@@@@&&&%. + ,(#&&@@@@@&&&&&&&&&&&&&&%%%%%%%%%%%&%# + ,(#&&@@@@@@@&&&&&&&&&&&&&&&&%%%&&%%&%# + (##&@########%%%%%%%%%%%%%%%%%%%%# + /(#,@@&(((((((((% @@@&(((((((((((/ + *(#.@@@(/#%#(/(( &@@@(((((((((((* + *((%..%&@@@@@%((% %(((((((((((* . + ,((((((&@@@@@@(/(((((((((((((((((*%/* + ,(((##((&@@@@(/(#@@(((((((((((((/((/* + ,((((((%((((((#@#(((((((((((((((/%//* + */((((((#&&%(((((((((((((((((((/&//* + *&@@@@@########%&@#&@&%%###(/#&/. + ...%&&@&%%.........@@&@@%,.,..,/#@@@( + .*%&&@&*.........%&%%%#... + + +\033[1;4mAvailable Commands\033[0m +pulse Runs diagnostic tests to check the overall health of your Looker instance +analyze [projects | models | explores] Analyses projects, models and explores to help identify model bloat +vacuum [models | explores] Identifies and outputs a list of unused content in models and explores + +\033[1;4mGlobal Options\033[0m + \033[1m--host\033[0m \033[4mhost\033[0m Looker host in the form of hostname.looker.com + \033[1m--port\033[0m \033[4mport\033[0m The port for API requests (default: 19999) + \033[1m--client_id\033[0m \033[4mclient_id\033[0m API3 client_id + \033[1m--client_secret\033[0m \033[4mclient_secret\033[0m API3 client_secret + \033[1m--path\033[0m \033[4mpath\033[0m Specify config file path. Defaults to user's current working directory. + \033[1m--output\033[0m \033[4mpath\033[0m Save output to file + \033[1m--alias\033[0m \033[4malias\033[0m Store auth credentials in config file under specified alias + + \033[1m--plain\033[0m Suppress table headers and format lines + \033[1m-q, --quiet\033[0m Silence output + \033[1m-h, --help\033[0m + +Run `henry --help` for help with a specific command. diff --git a/henry/.support_files/logging.conf b/henry/.support_files/logging.conf new file mode 100644 index 0000000..88d372a --- /dev/null +++ b/henry/.support_files/logging.conf @@ -0,0 +1,72 @@ +[loggers] +keys=root,lookerapi,fetcher,analyze,vacuum + +[handlers] +keys=rootHandler,apiHandler,fetcherHandler,analyzeHandler,vacuumHandler + +[formatters] +keys=simpleFormatter + +[logger_root] +level=DEBUG +handlers=rootHandler +qualname=main +propagate=0 + +[logger_lookerapi] +level=DEBUG +handlers=apiHandler +qualname=lookerapi +propagate=0 + +[logger_fetcher] +level=DEBUG +handlers=fetcherHandler +qualname=fetcher +propagate=0 + +[logger_analyze] +level=DEBUG +handlers=analyzeHandler +qualname=analyze +propagate=0 + +[logger_vacuum] +level=DEBUG +handlers=vacuumHandler +qualname=vacuum +propagate=0 + +[handler_rootHandler] +class=handlers.RotatingFileHandler +level=DEBUG +formatter=simpleFormatter +args=('%(logfilename)s', 'a', 500000, 10) + +[handler_fetcherHandler] +class=handlers.RotatingFileHandler +level=DEBUG +formatter=simpleFormatter +args=('%(logfilename)s', 'a', 500000, 10) + +[handler_analyzeHandler] +class=handlers.RotatingFileHandler +level=DEBUG +formatter=simpleFormatter +args=('%(logfilename)s', 'a', 500000, 10) + +[handler_apiHandler] +class=handlers.RotatingFileHandler +level=DEBUG +formatter=simpleFormatter +args=('%(logfilename)s', 'a', 500000, 10) + +[handler_vacuumHandler] +class=handlers.RotatingFileHandler +level=DEBUG +formatter=simpleFormatter +args=('%(logfilename)s', 'a', 500000, 10) + +[formatter_simpleFormatter] +format: %(asctime)s.%(msecs)03d [%(levelname)s|%(name)s] :: %(message)s +datefmt=%Y-%m-%d %H:%M:%S diff --git a/henry/__init__.py b/henry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/henry/__version__.py b/henry/__version__.py new file mode 100644 index 0000000..10939f0 --- /dev/null +++ b/henry/__version__.py @@ -0,0 +1 @@ +__version__ = '0.1.2' diff --git a/henry/cli.py b/henry/cli.py new file mode 100755 index 0000000..12898a3 --- /dev/null +++ b/henry/cli.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +from .modules.lookerapi import LookerApi +import argparse +import os +import errno +import sys +from .modules.spinner import Spinner +from .modules.auth import authenticate +import logging.config +import henry +from pathlib import PosixPath +import json +import uuid +from tabulate import tabulate +from .modules import data_controller as dc +import csv +from . import __version__ as pkg +LOGGING_CONFIG_PATH = os.path.join(os.path.dirname(henry.__file__), + '.support_files/logging.conf') +METADATA_PATH = os.path.join(os.path.expanduser('~'), '.henry') +if not os.path.exists(METADATA_PATH): + os.mkdir(METADATA_PATH) +elif os.path.exists(METADATA_PATH) and not os.path.isdir(METADATA_PATH): + print('Cannot create metadata directory in %s' % METADATA_PATH) + sys.exit(1) +LOGGING_LOG_PATH = os.path.join(METADATA_PATH, 'log') +if not os.path.exists(LOGGING_LOG_PATH): + os.mkdir(LOGGING_LOG_PATH) +elif os.path.exists(LOGGING_LOG_PATH) and not os.path.isdir(LOGGING_LOG_PATH): + print('Cannot create log directory in %s' % LOGGING_LOG_PATH) + sys.exit(1) +LOGGING_LOG_PATH = os.path.join(LOGGING_LOG_PATH, 'henry.log') +logging.config.fileConfig(LOGGING_CONFIG_PATH, + defaults={'logfilename': LOGGING_LOG_PATH}, + disable_existing_loggers=False) +from .commands.analyze import Analyze +from .commands.vacuum import Vacuum +from .commands.pulse import Pulse + +logger = logging.getLogger('main') +# sys.tracebacklimit = -1 # enable only on shipped release + + +def main(): + logger.info('Starting henry') + HELP_PATH = os.path.join(os.path.dirname(henry.__file__), + '.support_files/help.rtf') + with open(HELP_PATH, 'r', encoding='unicode_escape') as myfile: + descStr = myfile.read() + + # load custom config settings if defined in ~/.henry/henry.json + settings_file = PosixPath(os.path.join(METADATA_PATH, 'settings.json')).expanduser() + timeout = 120 + config_path = PosixPath.cwd().joinpath('config.yml') + if settings_file.is_file(): + with open(settings_file, 'r') as f: + settings = json.load(f) + timeout = settings.get('api_conn_timeout', timeout) + if type(timeout) is list: + timeout = tuple(timeout) + config_path = settings.get('config_path', config_path) + logger.info(f'Loaded config settings from ~/.henry/settings.json, {settings}') + else: + logger.info('No custom config file found. Using defaults.') + + parser = argparse.ArgumentParser( + description=descStr, + formatter_class=argparse.RawDescriptionHelpFormatter, + prog='henry', + usage='henry command subcommand ' + '[subcommand options] [global ' + 'options]', + allow_abbrev=False, + add_help=False) + + subparsers = parser.add_subparsers(dest='command', + help=argparse.SUPPRESS) + parser.add_argument("-h", "--help", action="help", help=argparse.SUPPRESS) + + # subparsers.required = True # works, but might do without for now. + + pulse = subparsers.add_parser('pulse', help='pulse help') + + analyze_parser = subparsers.add_parser('analyze', help='analyze help', + usage='henry analyze') + analyze_parser.set_defaults(which=None) + analyze_subparsers = analyze_parser.add_subparsers() + analyze_projects = analyze_subparsers.add_parser('projects') + analyze_models = analyze_subparsers.add_parser('models') + analyze_explores = analyze_subparsers.add_parser('explores') + + # project subcommand + analyze_projects.set_defaults(which='projects') + analyze_projects.add_argument('-p', '--project', + type=str, + default=None, + help='Filter on a project') + analyze_projects.add_argument('--order_by', + nargs=2, + metavar=('ORDER_FIELD', 'ASC/DESC'), + dest='sortkey', + help='Sort results by a field') + analyze_projects.add_argument('--limit', + type=int, + default=None, + nargs=1, + help='Limit results. No limit by default') + + # models subcommand + analyze_models.set_defaults(which='models') + models_group = analyze_models.add_mutually_exclusive_group() + + models_group.add_argument('-p', '--project', + type=str, + default=None, + help='Filter on project') + models_group.add_argument('-model', '--model', + type=str, + default=None, + help='Filter on model') + analyze_models.add_argument('--timeframe', + type=int, + default=90, + help='Timeframe (between 0 and 90)') + analyze_models.add_argument('--min_queries', + type=int, + default=0, + help='Query threshold') + analyze_models.add_argument('--order_by', + nargs=2, + metavar=('ORDER_FIELD', 'ASC/DESC'), + dest='sortkey', + help='Sort results by a field') + analyze_models.add_argument('--limit', + type=int, + default=None, + nargs=1, + help='Limit results. No limit by default') + + # explores subcommand + analyze_explores.set_defaults(which='explores') + analyze_explores.add_argument('-model', '--model', + type=str, + default=None, + required=('--explore') in sys.argv, + help='Filter on model') + analyze_explores.add_argument('-e', '--explore', + default=None, + help='Filter on model') + analyze_explores.add_argument('--timeframe', + type=int, + default=90, + help='Timeframe (between 0 and 90)') + analyze_explores.add_argument('--min_queries', + type=int, + default=0, + help='Query threshold') + analyze_explores.add_argument('--order_by', + nargs=2, + metavar=('ORDER_FIELD', 'ASC/DESC'), + dest='sortkey', + help='Sort results by a field') + analyze_explores.add_argument('--limit', + type=int, + default=None, + nargs=1, + help='Limit results. No limit by default') + + # VACUUM Subcommand + vacuum_parser = subparsers.add_parser('vacuum', help='vacuum help', + usage='henry vacuum') + vacuum_parser.set_defaults(which=None) + vacuum_subparsers = vacuum_parser.add_subparsers() + vacuum_models = vacuum_subparsers.add_parser('models') + vacuum_explores = vacuum_subparsers.add_parser('explores') + vacuum_models.set_defaults(which='models') + vm_group = vacuum_models.add_mutually_exclusive_group() + vm_group.add_argument('-p', '--project', + type=str, + default=None, + help='Filter on Project') + vm_group.add_argument('-m', '--model', + type=str, + default=None, + help='Filter on model') + + vacuum_models.add_argument('--timeframe', + type=int, + default=90, + help='Usage period to examine (in the range of ' + '0-90 days). Default: 90 days.') + + vacuum_models.add_argument('--min_queries', + type=int, + default=0, + help='Vacuum threshold. Explores with less ' + 'queries in the given usage period will ' + 'be vacuumed. Default: 0 queries.') + + vacuum_explores.set_defaults(which='explores') + vacuum_explores.add_argument('-m', '--model', + type=str, + default=None, + required=('--explore') in sys.argv, + help='Filter on model') + + vacuum_explores.add_argument('-e', '--explore', + type=str, + default=None, + help='Filter on explore') + + vacuum_explores.add_argument('--timeframe', + type=int, + default=90, + help='Timeframe (between 0 and 90)') + + vacuum_explores.add_argument('--min_queries', + type=int, + default=0, + help='Query threshold') + + for subparser in [analyze_projects, analyze_models, analyze_explores, + vacuum_models, vacuum_explores, pulse]: + subparser.add_argument('--output', + type=str, + default=None, + help='Path to file for saving the output') + subparser.add_argument('-q', '--quiet', + action='store_true', + help='Silence output') + subparser.add_argument('--plain', + default=None, + action='store_true', + help='Show results in a table format ' + 'without the gridlines') + subparser.add_argument_group("Authentication") + subparser.add_argument('--host', type=str, default='looker', + required=any(k in sys.argv for k in + ['--client_id', '--client_secret', + '--alias']), + help=argparse.SUPPRESS) + subparser.add_argument('--port', type=int, default=19999, + help=argparse.SUPPRESS) + subparser.add_argument('--client_id', type=str, + required=any(k in sys.argv for k in + ['--client_secret', '--alias']), + help=argparse.SUPPRESS) + subparser.add_argument('--client_secret', type=str, + required=any(k in sys.argv for k in + ['--client_id', '--alias']), + help=argparse.SUPPRESS) + subparser.add_argument('--alias', type=str, + help=argparse.SUPPRESS) + subparser.add_argument('--path', type=str, default='', + help=argparse.SUPPRESS) + + args = vars(parser.parse_args()) + _args = {} + for key, value in args.items(): + if key == 'client_secret': + _args[key] = '[FILTERED]' + else: + _args[key] = value + logger.info('Parsing args, %s', _args) + + if not args['command']: + print('usage:', parser.usage) + print('\nNo command specified. Try `henry --help` for help.') + sys.exit(1) + auth_params = ('host', 'port', 'client_id', 'client_secret', + 'alias', 'path') + auth_args = {k: args[k] for k in auth_params} + + # authenticate + if args['command'] != 'pulse': + cmd = args['command']+' '+args['which'] + else: + cmd = args['command'] + session_info = f'Henry v{pkg.__version__}: cmd={cmd}' \ + f', sid=#{uuid.uuid1()}' + looker = authenticate(timeout, session_info, config_path, **auth_args) + + # map subcommand to function + if args['command'] in ('analyze', 'vacuum'): + if args['which'] is None: + parser.error("No command") + else: + with Spinner(): + if args['command'] == 'analyze': + analyze = Analyze(looker) + result = analyze.analyze(**args) + else: + vacuum = Vacuum(looker) + result = vacuum.vacuum(**args) + # print results if --silence flag is not used + if not args['quiet']: + # tabulate result and print + tablefmt = 'plain' if args['plain'] else 'psql' + headers = 'keys' + formatted_result = tabulate(result, headers=headers, + tablefmt=tablefmt, numalign='center') + print(formatted_result) + + elif args['command'] == 'pulse': + pulse = Pulse(looker) + result = pulse.run_all() + else: + print('No command passed') + + # save to file if --output flag is used + if args['output']: + file = args['output'] + logger.info(f'Saving results to {file}') + dc.save_to_file(args['output'], result) + logger.info('Results succesfully saved.') + + +if __name__ == "__main__": + main() diff --git a/henry/commands/__init__.py b/henry/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/henry/commands/analyze.py b/henry/commands/analyze.py new file mode 100644 index 0000000..d48dd53 --- /dev/null +++ b/henry/commands/analyze.py @@ -0,0 +1,168 @@ +import logging +from henry.modules.fetcher import Fetcher as fetcher +from henry.modules import data_controller as dc +from tabulate import tabulate +import json + + +class Analyze(fetcher): + def __init__(self, looker): + super().__init__(looker) + self.analyze_logger = logging.getLogger('analyze') + + def analyze(self, **kwargs): + format = 'plain' if kwargs['plain'] else 'psql' + headers = '' if kwargs['plain'] else 'keys' + p = kwargs['project'] if 'project' in kwargs.keys() else None + m = kwargs['model'] if 'model' in kwargs.keys() else None + self.analyze_logger.info('Analyzing %s', kwargs['which'].capitalize()) + if kwargs['which'] == 'projects': + params = {k: kwargs[k] for k in {'project', 'sortkey', 'limit'}} + self.analyze_logger.info('analyze projects params=%s', params) + result = self._analyze_projects(project=p, + sortkey=kwargs['sortkey'], + limit=kwargs['limit']) + elif kwargs['which'] == 'models': + params = {k: kwargs[k] for k in {'project', + 'model', + 'timeframe', + 'min_queries', + 'sortkey', + 'limit'}} + self.analyze_logger.info('analyze models params=%s', params) + result = self._analyze_models(project=p, + model=m, + sortkey=kwargs['sortkey'], + limit=kwargs['limit'], + timeframe=kwargs['timeframe'], + min_queries=kwargs['min_queries']) + elif kwargs['which'] == 'explores': + params = {k: kwargs[k] for k in {'model', + 'explore', + 'timeframe', + 'min_queries', + 'sortkey', + 'limit'}} + self.analyze_logger.info('analyze explores params=%s', ) + result = self._analyze_explores(model=m, + explore=kwargs['explore'], + sortkey=kwargs['sortkey'], + limit=kwargs['limit'], + timeframe=kwargs['timeframe'], + min_queries=kwargs['min_queries']) + self.analyze_logger.info('Analyze Complete') + + return result + + def _analyze_projects(self, project=None, sortkey=None, limit=None): + projects = fetcher.get_project_files(self, project=project) + info = [] + for p in projects: + metadata = list(map(lambda x: + 'model' if x['type'] == 'model' else + ('view' if x['type'] == 'view' else None), + p['files'])) + + model_count = metadata.count('model') + view_count = metadata.count('view') + git_tests = fetcher.test_git_connection(self, p['name']) + info.append({ + 'project': p['name'], + 'model_count': model_count, + 'view_count': view_count, + 'git_connection_status': git_tests, + 'pull_request_mode': p['pr_mode'], + 'validation_required': p['validation_required'] + }) + + valid_values = list(info[0].keys()) + info = dc.sort(info, valid_values, sortkey) + info = dc.limit(info, limit=limit) + + return info + + def _analyze_models(self, project=None, model=None, + sortkey=None, limit=None, + timeframe=90, min_queries=0): + models = fetcher.get_models(self, project=project, + model=model, verbose=1) + used_models = fetcher.get_used_models(self, timeframe, min_queries) + info = [] + for m in models: + explore_count = len(m['explores']) + if m['name'] in used_models: + query_run_count = used_models[m['name']] + else: + query_run_count = 0 + unused_explores = fetcher.get_unused_explores(self, m['name'], + timeframe, + min_queries) + info.append({ + 'project': m['project_name'], + 'model': m['name'], + 'explore_count': explore_count, + 'unused_explores': len(unused_explores), + 'query_run_count': query_run_count + }) + valid_values = list(info[0].keys()) + info = dc.sort(info, valid_values, sortkey) + info = dc.limit(info, limit=limit) + return info + + def _analyze_explores(self, model=None, explore=None, + sortkey=None, limit=None, + min_queries=0, timeframe=90): + explores = fetcher.get_explores(self, model=model, + explore=explore, verbose=1) + explores_usage = {} + info = [] + for e in explores: + # in case explore does not exist (bug - #32748) + if e is None: + pass + else: + _used_fields = fetcher.get_used_explore_fields(self, + e['model_name'], + e['scopes'], + timeframe, + min_queries) + used_fields = list(_used_fields.keys()) + exposed_fields = fetcher.get_explore_fields(self, + explore=e, + scoped_names=1) + unused_fields = set(exposed_fields) - set(used_fields) + field_count = len(exposed_fields) + query_count = fetcher.get_used_explores(self, + model=e['model_name'], + explore=e['name']) + + all_joins = set(e['scopes']) + all_joins.remove(e['name']) + used_joins = set([i.split('.')[2] for i in used_fields]) + unused_joins = len(list(all_joins - used_joins)) + + has_description = 'Yes' if e['description'] else 'No' + + if query_count.get(e['name']): + query_count = query_count[e['name']] + else: + query_count = 0 + info.append({ + 'model': e['model_name'], + 'explore': e['name'], + 'is_hidden': e['hidden'], + 'has_description': has_description, + 'join_count': len(all_joins), + 'unused_joins': unused_joins, + 'field_count': field_count, + 'unused_fields': len(unused_fields), + 'query_count': query_count + }) + + if not info: + self.analyze_logger.error('No matching explores found') + raise Exception('No matching explores found') + valid_values = list(info[0].keys()) + info = dc.sort(info, valid_values, sortkey) + info = dc.limit(info, limit=limit) + return info diff --git a/henry/commands/pulse.py b/henry/commands/pulse.py new file mode 100644 index 0000000..3ee5913 --- /dev/null +++ b/henry/commands/pulse.py @@ -0,0 +1,353 @@ +import logging +import re +import requests +from textwrap import fill +from tqdm import tqdm +from tabulate import tabulate +from tqdm import trange +from henry.modules.color import color + + +class Pulse(object): + + postfix_default = [dict(value="RUNNING")] + + def __init__(self, looker): + self.looker = looker + self.pulse_logger = logging.getLogger('pulse') + self.bar = '%s%s{postfix[0][value]}%s {desc}: ' \ + '{percentage:3.0f}%% |{bar}|[{elapsed}<' \ + '{remaining}]' % (color.BOLD, + color.GREEN, + color.ENDC) + self.postfix_default = [dict(value="RUNNING")] + + def run_all(self): + self.pulse_logger.info('Checking instance pulse') + self.pulse_logger.info('Checking Connections') + result = self.check_connections() + print(result, end='\n\n') + self.pulse_logger.info('Complete: Checking Connections') + + self.pulse_logger.info('Analyzing Query Stats') + r1, r2, r3 = self.check_query_stats() + print(r1) + print(r2) + print(r3, end='\n\n') + self.pulse_logger.info('Complete: Analyzing Query Stats') + + # check scheduled plans + self.pulse_logger.info('Analyzing Query Stats') + with trange(1, desc='(3/5) Analyzing Scheduled Plans', + bar_format=self.bar, postfix=self.postfix_default, + ncols=100, miniters=0) as t: + for i in t: + result = self.check_scheduled_plans() + fail_flag = 0 + if type(result) == list and len(result) > 0: + if result[0]['failure'] > 0: + fail_flag = 1 + result = tabulate(result, headers="keys", + tablefmt='psql', numalign='center') + t.postfix[0]["value"] = 'DONE' + t.update() + print(result, end='\n\n') + if fail_flag == 1: + print('Navigate to /admin/scheduled_jobs on your instance for ' + 'more details', end='\n\n') + self.pulse_logger.info('Complete: Analyzing Scheduled Plans') + + # check enabled legacy features + self.pulse_logger.info('Checking Legacy Features') + with trange(1, desc='(4/5) Legacy Features', bar_format=self.bar, + postfix=self.postfix_default, ncols=100, miniters=0) as t: + for i in t: + result = self.check_legacy_features() + t.postfix[0]["value"] = 'DONE' + t.update() + print(result, end='\n\n') + self.pulse_logger.info('Complete: Checking Legacy Features') + + # check looker version + self.pulse_logger.info('Checking Version') + t = trange(1, desc='(5/5) Version', bar_format=self.bar, + postfix=self.postfix_default, ncols=100) + for i in t: + result = self.check_version() + t.postfix[0]["value"] = "DONE" + t.update() + print(result, end='\n\n') + self.pulse_logger.info('Complete: Checking Version') + self.pulse_logger.info('Complete: Checking instance pulse') + + return + + def check_connections(self): + result = [] + connections = [] + for c in self.looker.get_connections(): + if c['name'] != 'looker': + c_tests = (', ').join(c['dialect']['connection_tests']) + c_name = c['name'] + connections.append((c_name, c_tests)) + + with tqdm(total=len(connections), desc='(1/5) Testing Connections', + bar_format=self.bar, postfix=self.postfix_default, + ncols=100, miniters=0) as t: + for idx, (c, tests) in enumerate(connections): + tests = {'tests': tests} + results = self.looker.test_connection(c, tests) + formatted_results = [] + fail_flag = 0 + for i in results: + if i['status'] == 'error': + formatted_results.append('-- ' + fill(i['message'], + width=100)) + fail_flag = 1 + formatted_results = list(set(formatted_results)) + status = '\n'.join(formatted_results) + result.append({'Connection': c, + 'Status': 'OK' if fail_flag == 0 else status}) + if idx == len(connections) - 1: + t.postfix[0]['value'] = 'DONE' + t.update() + + return tabulate(result, headers="keys", tablefmt='psql') + + def check_query_stats(self): + # check query stats + with trange(3, desc='(2/5) Analyzing Query Stats', bar_format=self.bar, + postfix=self.postfix_default, ncols=100, miniters=0) as t: + for i in t: + if i == 0: + query_count = self.get_query_type_count() + if i == 1: + query_runtime_stats = self.get_query_stats('complete') + if i == 2: + slow_queries = self.get_slow_queries( + query_runtime_stats['avg']*5) + t.postfix[0]['value'] = 'DONE' + + r1 = '{} queries run, ' \ + '{} errored, ' \ + '{} killed'.format(query_count['total'], query_count['errored'], + query_count['killed']) + r2 = 'Query Runtime min/avg/max: ' \ + '{}/{}/{} seconds'.format(query_runtime_stats['min'], + query_runtime_stats['avg'], + query_runtime_stats['max']) + + if slow_queries: + r3 = 'Query IDs for queries that took more than 5x the average ' \ + 'query runtime : {}'.format(slow_queries) + r3 = fill(r3, width=80) + else: + r3 = 'No abnormally slow queries found' + return r1, r2, r3 + + # get number of queries run, killed, completed, errored, queued + def get_slow_queries(self, avg_runtime): + body = { + "model": "i__looker", + "view": "history", + "fields": [ + "query.id", + ], + "filters": { + "query.id": "NOT NULL", + "history.created_date": "30 days", + "history.status": "-NULL", + "history.result_source": "query", + "query.model": "-i^_^_looker", + "history.total_runtime": ">=" + str(avg_runtime) + }, + "sorts": [ + "query.id asc" + ], + "limit": "50000" + } + + r = self.looker.run_inline_query(result_format="json", body=body, + fields={"cache": "false"}) + + if r: + ids = (', ').join([str(query['query.id']) for query in r]) + else: + ids = None + return ids + + # get number of queries run, killed, completed, errored, queued + def get_query_type_count(self): + body = { + "model": "i__looker", + "view": "history", + "fields": [ + "history.query_run_count", + "history.status", + "history.created_date" + ], + "pivots": [ + "history.status" + ], + "filters": { + "history.created_date": "30 days", + "history.status": "-NULL", + "history.result_source": "query", + "query.model": "-i^_^_looker" + }, + "sorts": [ + "history.created_date desc", + "history.result_source" + ], + "limit": "50000" + } + + r = self.looker.run_inline_query(result_format="json", body=body, + fields={"cache": "false"}) + completed = 0 + errored = 0 + killed = 0 + queued = 0 + if(len(r) > 0): + for entry in r: + e = entry['history.query_run_count']['history.status'] + if 'complete' in e: + c_i = e['complete'] + else: + c_i = 0 + c_i = c_i if c_i is not None else 0 + completed += c_i + + if 'error' in e: + e_i = e['error'] + else: + e_i = 0 + e_i = e_i if e_i is not None else 0 + errored += e_i + + if 'killed' in e: + k_i = e['killed'] + else: + k_i = 0 + k_i = k_i if k_i is not None else 0 + killed += k_i + + if 'pending' in e: + q_i = e['pending'] + else: + q_i = 0 + q_i = q_i if q_i is not None else 0 + queued += q_i + + response = {'total': completed + errored + killed, + 'completed': completed, + 'errored': errored, + 'killed': killed, + 'queued': queued} + + return response + + # get number of queries run, killed, completed, errored, queued + def get_query_stats(self, status): + valid_statuses = ['error', 'complete', 'running'] + if status not in valid_statuses: + raise ValueError("Invalid query status, must be in %r" + % valid_statuses) + body = { + "model": "i__looker", + "view": "history", + "fields": [ + "history.min_runtime", + "history.max_runtime", + "history.average_runtime", + "history.total_runtime" + ], + "filters": { + "history.created_date": "30 days", + "history.status": status, + "query.model": "-i^_^_looker" + }, + "limit": "50000" + } + + r = self.looker.run_inline_query(result_format="json", body=body, + fields={"cache": "false"})[0] + for i in ('history.min_runtime', + 'history.max_runtime', + 'history.average_runtime'): + if r[i] is not None: + r[i] = round(r[i], 2) + else: + r[i] = '-' + response = {'min': r['history.min_runtime'], + 'max': r['history.max_runtime'], + 'avg': r['history.average_runtime'], + 'total': r['history.total_runtime']} + + return response + + def check_scheduled_plans(self): + body = { + "model": "i__looker", + "view": "scheduled_plan", + "fields": ["scheduled_job.status", "scheduled_job.count"], + "pivots": ["scheduled_job.status"], + "filters": { + "scheduled_plan.run_once": "no", + "scheduled_job.status": "-NULL", + "scheduled_job.created_date": "30 days" + }, + "limit": "50000" + } + + r = self.looker.run_inline_query("json", body) + result = [] + if r: + r = r[0]['scheduled_job.count']['scheduled_job.status'] + failed = r['failure'] if 'failure' in r.keys() else 0 + succeeded = r['success'] if 'success' in r.keys() else 0 + result.append({'total': failed + succeeded, + 'failure': failed, + 'success': succeeded}) + return result + else: + return "No Plans Found" + + def check_integrations(self): + response = self.looker.get_integrations() + integrations = [] + for r in response: + if r['enabled']: + integrations.append(r['label']) + + result = None if len(integrations) == 0 else integrations + + return result + + def check_legacy_features(self): + response = self.looker.get_legacy_features() + _result = [] + for r in response: + if r['enabled'] is True: + _result.append({'Legacy Features': r['name']}) + + if _result: + result = tabulate(_result, headers="keys", tablefmt='psql') + else: + result = 'No legacy features found' + return result + + def check_version(self): + _v = self.looker.get_version()['looker_release_version'] + version = re.findall(r'(\d.\d+)', _v)[0] + session = requests.Session() + _lv = session.get('https://learn.looker.com:19999/versions').json() + _lv = _lv['looker_release_version'] + latest_version = re.findall(r'(\d.\d+)', _lv)[0] + if version == latest_version: + result = '{} ({})'.format(version, 'up-to-date') + else: + result = '{} ({} {})'.format(version, + 'outdated, latest version is', + latest_version) + return result diff --git a/henry/commands/vacuum.py b/henry/commands/vacuum.py new file mode 100644 index 0000000..6ec8d68 --- /dev/null +++ b/henry/commands/vacuum.py @@ -0,0 +1,124 @@ +import logging +from henry.modules import data_controller as dc +from henry.modules.fetcher import Fetcher as fetcher +from henry.modules.color import color +import re + + +class Vacuum(fetcher): + def __init__(self, looker): + super().__init__(looker) + self.vacuum_logger = logging.getLogger('vacuum') + + def vacuum(self, **kwargs): + p = kwargs['project'] if 'project' in kwargs.keys() else None + m = kwargs['model'] if 'model' in kwargs.keys() else None + format = 'plain' if kwargs['plain'] else 'psql' + headers = '' if kwargs['plain'] else 'keys' + if kwargs['which'] == 'models': + self.vacuum_logger.info('Vacuuming Models') + params = {k: kwargs[k] for k in {'project', + 'model', + 'timeframe', + 'min_queries'}} + self.vacuum_logger.info('vacuum models params=%s', params) + result = self._vacuum_models(project=p, + model=m, + min_queries=kwargs['min_queries'], + timeframe=kwargs['timeframe']) + if kwargs['which'] == 'explores': + self.vacuum_logger.info('Vacuuming Explores') + params = {k: kwargs[k] for k in {'model', + 'explore', + 'timeframe', + 'min_queries'}} + self.vacuum_logger.info('vacuum explores params=%s', params), + result = self._vacuum_explores(model=m, + explore=kwargs['explore'], + min_queries=kwargs['min_queries'], + timeframe=kwargs['timeframe']) + self.vacuum_logger.info('Vacuum Complete') + + return result + + def _vacuum_models(self, project=None, model=None, timeframe=90, + min_queries=0): + if model is None: + model = fetcher.get_models(self, project=project) + else: + model = model.split() + used_models = fetcher.get_used_models(self, timeframe) + info = [] + for m in model: + explores = [e['name'] for e in fetcher.get_explores(self, model=m, + verbose=1)] + unused_explores = fetcher.get_unused_explores(self, m, + timeframe, + min_queries) + query_run_count = used_models[m] if m in used_models.keys() else 0 + unused_explores = ('\n').join(unused_explores) + info.append({ + 'model': m, + 'unused_explores': unused_explores or 'None', + 'model_query_run_count': query_run_count}) + + return info + + def _vacuum_explores(self, model=None, explore=None, timeframe=90, + min_queries=0): + explores = fetcher.get_explores(self, + model=model, + explore=explore, + verbose=1) + info = [] + for e in explores: + # get field usage from i__looker using all the views inside explore + # returns fields in the form of model.explore.view.field + _used_fields = fetcher.get_used_explore_fields(self, + e['model_name'], + e['scopes'], + timeframe, + min_queries) + used_fields = list(_used_fields.keys()) + # get field picker fields in the form of model.explore.view.field + exposed_fields = fetcher.get_explore_fields(self, + explore=e, + scoped_names=1) + _unused_fields = set(exposed_fields) - set(used_fields) + + # remove scoping + all_joins = set(e['scopes']) + all_joins.remove(e['name']) + used_joins = set([i.split('.')[2] for i in used_fields]) + + _unused_joins = list(all_joins - used_joins) + unused_joins = ('\n').join(_unused_joins) or 'N/A' + + # only keep fields that belong to used joins (unused joins fields + # don't matter) if there's at least one used join (including the + # base view). else don't match anything + temp = list(used_joins) + temp.append(e['name']) + pattern = ('|').join(temp) or 'ALL' + unused_fields = [] + if pattern != 'ALL': + for field in _unused_fields: + f = re.match(r'^({0}).*'.format(pattern), + '.'.join(field.split('.')[2:])) + if f is not None: + unused_fields.append(f.group(0)) + unused_fields = sorted(unused_fields) + unused_fields = ('\n').join(unused_fields) + else: + unused_fields = color.format(pattern, 'fail', 'color') + + info.append({ + 'model': e['model_name'], + 'explore': e['name'], + 'unused_joins': unused_joins, + 'unused_fields': unused_fields + }) + if not info: + self.vacuum_logger.error('No matching explores found') + raise Exception('No matching explores found') + return info diff --git a/henry/modules/__init__.py b/henry/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/henry/modules/auth.py b/henry/modules/auth.py new file mode 100644 index 0000000..69c27ad --- /dev/null +++ b/henry/modules/auth.py @@ -0,0 +1,81 @@ +import os +import logging +import yaml +import sys +from .lookerapi import LookerApi + +auth_logger = logging.getLogger('auth') + + +# returns an instanstiated Looker object using the +# credentials supplied by the auth argument group +def authenticate(timeout, session_info, config_path, **kwargs): + auth_logger.info('Authenticating into Looker API') + # precedence: --path, global config, default + cleanpath = kwargs['path'] or config_path + if kwargs['client_id'] and kwargs['client_secret']: + auth_logger.info('Fetching auth params passed in CLI') + host = kwargs['host'] + client_id = kwargs['client_id'] + client_secret = kwargs['client_secret'] + token = None + else: + auth_logger.info('Checking permissions for %s', cleanpath) + st = os.stat(cleanpath) + ap = oct(st.st_mode) + if ap != '0o100600': + print(f'Config file permissions are set to %s and are not strict ' + 'enough. Change to rw------- or 600 and try again.' % ap) + auth_logger.warning('Config file permissions are %s and not strict' + ' enough.' % ap) + sys.exit(1) + auth_logger.info('Opening config file from %s' % cleanpath) + try: + f = open(cleanpath, 'r') + params = yaml.safe_load(f) + f.close() + except FileNotFoundError as error: + auth_logger.exception(error, exc_info=False) + print('ERROR: %s not found' % filepath) + sys.exit(1) + + try: + auth_logger.info('Fetching auth credentials from file, %s', + cleanpath) + host = params['hosts'][kwargs['host']]['host'] + client_secret = params['hosts'][kwargs['host']]['secret'] + client_id = params['hosts'][kwargs['host']]['id'] + except KeyError as error: + auth_logger.error('Auth Error: %s not found' % error, + exc_info=False) + print('ERROR: %s not found' % error) + sys.exit(1) + + auth_logger.info('auth params=%s', {'host': host, + 'port': kwargs['port'], + 'client_id': client_id, + 'client_secret': "[FILTERED]"}) + looker = LookerApi(host=host, + port=kwargs['port'], + id=client_id, + secret=client_secret, + timeout=timeout, + session_info=session_info, + ) + auth_logger.info('Authentication Successful') + + if kwargs['alias']: + auth_logger.info('Saving credentials to file: %s', cleanpath) + with open(cleanpath, 'r') as f: + params = yaml.safe_load(f) + params['hosts'][kwargs['alias']] = {} + params['hosts'][kwargs['alias']]['host'] = host + params['hosts'][kwargs['alias']]['id'] = client_id + params['hosts'][kwargs['alias']]['secret'] = client_secret + + with open(cleanpath, 'w') as f: + yaml.safe_dump(params, f, default_flow_style=False) + + os.chmod(cleanpath, 0o600) + + return looker diff --git a/henry/modules/color.py b/henry/modules/color.py new file mode 100644 index 0000000..3a07cab --- /dev/null +++ b/henry/modules/color.py @@ -0,0 +1,32 @@ +# color.py + + +# string is the original string +# type determines the color/label added +# style can be color or text +class color(object): + GREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + + def format(self, string, type, style='color'): + formatted_string = '' + string = str(string) + if style == 'color': + if type in ('success', 'pass'): + formatted_string += self.GREEN + string + self.ENDC + elif type == 'warning': + formatted_string += self.WARNING + string + self.ENDC + elif type in ('error', 'fail'): + formatted_string += self.FAIL + string + self.ENDC + elif style == 'text': + if type == 'success': + formatted_string += string + elif type == 'warning': + formatted_string += 'WARNING: ' + string + elif type == 'error': + formatted_string += 'ERROR: ' + string + + return formatted_string diff --git a/henry/modules/data_controller.py b/henry/modules/data_controller.py new file mode 100644 index 0000000..50e8873 --- /dev/null +++ b/henry/modules/data_controller.py @@ -0,0 +1,52 @@ +# data_controller.py +import logging +import csv +from operator import itemgetter + +dc_logger = logging.getLogger('dc') + + +def limit(data, limit=None): + if limit is not None: + dc_logger.info('Limiting results to %s', limit[0]) + return data[:limit[0]] + else: + return data + + +def sort(data, valid_values, sortkey): + if sortkey is None: + return data + else: + dc_logger.info('Sort params=> %s', sortkey) + valid_types = {'ASC': False, 'DESC': True} + if sortkey[1].upper() in valid_types.keys(): + type = valid_types[sortkey[1].upper()] + else: + type = None + + sk = sortkey[0] if sortkey[0] in valid_values else False + if not sk: + dc_logger.error('Sortkey:%s is invalid', sortkey[0]) + raise ValueError('Unrecognised order_by field, must be in %r' % + valid_values) + elif type is None: + dc_logger.error('Sort type is invalid') + raise ValueError('Unrecognised order_by field, must be in %r' % + list(valid_types.keys())) + else: + dc_logger.info('Sorting data by %s %s', sk, type) + data = sorted(data, key=itemgetter(sk), reverse=type) + return data + + +def save_to_file(filename, data): + if not filename.endswith('.csv'): + filename += '.csv' + try: + with open(filename, 'w', newline='') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=data[0].keys()) + writer.writeheader() + writer.writerows(data) + except csv.Error as e: + raise Exception(e) diff --git a/henry/modules/fetcher.py b/henry/modules/fetcher.py new file mode 100644 index 0000000..caa7c17 --- /dev/null +++ b/henry/modules/fetcher.py @@ -0,0 +1,260 @@ +from collections import Counter +import logging +import re + + +class Fetcher(object): + def __init__(self, looker): + self.looker = looker + self.fetch_logger = logging.getLogger('fetcher') + + def get_project_files(self, project=None): + self.fetch_logger.info('Fetching projects, %s', locals()) + if project is None: + self.fetch_logger.info('Fetching all project files') + projects = self.looker.get_projects() + else: + self.fetch_logger.info('Fetching project files for %s', project) + projects = self.looker.get_project(project) + + project_data = [] + for p in projects: + project_files = self.looker.get_project_files(project=p['id']) + + project_data.append({ + 'name': p['id'], + 'pr_mode': p['pull_request_mode'], + 'validation_required': p['validation_required'], + 'git_remote_url': p['git_remote_url'], + 'files': project_files + }) + self.fetch_logger.info('Fetch Complete :: Projects') + return project_data + + # function that returns list of model definitions or model names (with + # verbose 0 or 1 respectively) Allows the user to specify a project name, + # a model name or nothing at all. project paramater is a string while model + # parameter is a list + def get_models(self, project=None, model=None, verbose=0, scoped_names=0): + if project is None and model is None: + self.fetch_logger.info('Fetching all models, %s', locals()) + models = self.looker.get_models() + elif project is not None and model is None: + # if no parameters are specified + self.fetch_logger.info('Fetching all models in %s, %s', project, + locals()) + r = self.looker.get_models() + models = list(filter(lambda x: x['project_name'] == project, r)) + if not models: + self.fetch_logger.error('Project not found') + raise Exception('Project not found') + elif project is not None and model is not None: + # if both project and model paramaters are specified + self.fetch_logger.info('Warning: Project parameter ignored. \ + Model names are unique across projects.') + models = [self.looker.get_model(model)] + else: + # if project parameter wasn't passed but model was. + self.fetch_logger.info('Fetching model %s, %s', model, locals()) + models = self.looker.get_model(model_name=model) + + models = list(filter(lambda x: x['has_content'] is True, models)) + if verbose == 0: + models = [(m['project_name'] + ".") * scoped_names + m['name'] + for m in models] + self.fetch_logger.info('Fetch Complete :: Models') + return models + + def get_used_models(self, timeframe=90, min_queries=0): + self.fetch_logger.info('Fetching used models from i__looker, %s', + locals()) + timeframe = str(timeframe) + ' days' + min_queries = '>=' + str(min_queries) + body = { + "model": "i__looker", + "view": "history", + "fields": ["query.model", "history.query_run_count"], + "filters": {"history.created_date": timeframe, + "query.model": "-i^_^_looker", + "history.query_run_count": min_queries + }, + "limit": "50000" + } + + response = self.looker.run_inline_query("json", body) + + x = {} + for r in response: + x[r['query.model']] = r['history.query_run_count'] + self.fetch_logger.info('Used Models Fetch Complete') + return(x) + + # errors have to be handled more downstream if explore does not exist due + # to bug #32748 + def get_explores(self, model=None, explore=None, scoped_names=0, + verbose=0): + explores = [] + if explore is not None: + self.fetch_logger.info('Fetching explore %s, %s', explore, + locals()) + e = self.looker.get_explore(model_name=model, explore_name=explore) + if e: + explores.extend(e) + else: + self.fetch_logger.info('Fetching all explores, %s', locals()) + models = self.get_models(model=model, verbose=1) + for mdl in models: + for e in mdl['explores']: + if verbose == 1: + explores.extend(self.looker.get_explore(mdl['name'], + e['name'])) + else: + explores.append((mdl['name'], e['name'])) + self.fetch_logger.info('Fetch Complete :: Explores') + return explores + + def get_explore_fields(self, explore=None, scoped_names=0): + self.fetch_logger.info('Parsing explore body for exposed fields') + fields = [] + for dimension in explore['fields']['dimensions']: + if dimension['hidden'] is not True: + fields.append((explore['model_name']+'.' + + explore['name'] + '.')*scoped_names + + dimension['name']) + for measure in explore['fields']['measures']: + if measure['hidden'] is not True: + fields.append((explore['model_name']+'.' + + explore['name']+'.')*scoped_names + + measure['name']) + for fltr in explore['fields']['filters']: + if fltr['hidden'] is not True: + fields.append((explore['model_name']+'.' + + explore['name']+'.')*scoped_names + + fltr['name']) + self.fetch_logger.info('Parsing Complete') + return list(set(fields)) + + def get_unused_explores(self, model=None, timeframe=90, min_queries=0): + self.fetch_logger.info('Fetching unused explores, %s', locals()) + used_explores = self.get_used_explores(model=model, + timeframe=timeframe, + min_queries=min_queries) + used_explores = used_explores.keys() + all_explores = self.get_explores(model=model) + all_explores = [i[1] for i in all_explores] + unused_explores = list(set(all_explores) - set(used_explores)) + self.fetch_logger.info('Fetch Complete:: Unused Explores') + return unused_explores + + # function that runs i__looker query and returns fully scoped fields used + # remember explore names are not unique, filter on model as well + # query.explore is the actual explore name + # query.model is the model + # query.fields are is view.field (view is view name used in the explore) + # to uniquely identify fields, explore.view.field should be used, + # or even better, model.explore.view.field + def get_used_explore_fields(self, model=None, explore=None, timeframe=90, + min_queries=0): + self.fetch_logger.info('Fetching exposed explore fields, %s', locals()) + m = model.replace('_', '^_') + ',' if model is not None else '' + m += "-i^_^_looker" + e = ','.join(explore).replace('_', '^_') + min_queries = '>=' + str(min_queries) + timeframe = str(timeframe) + ' days' + body = { + "model": "i__looker", + "view": "history", + "fields": ["query.model", "query.view", + "query.formatted_fields", + "query.formatted_filters", "query.sorts", + "query.formatted_pivots", + "history.query_run_count"], + "filters": {"history.created_date": timeframe, + "query.model": m, + "query.view": e, + "history.query_run_count": min_queries}, + "limit": "50000" + } + # returns only fields used from a given explore + response = self.looker.run_inline_query("json", body) + + formatted_fields = [] + for row in response: + fields = [] + explore = row['query.view'] + model = row['query.model'] + run_count = row['history.query_run_count'] + fields.extend(re.findall(r'(\w+\.\w+)', + str(row['query.formatted_fields']))) + fields.extend(re.findall(r'(\w+\.\w+)', + str(row['query.formatted_filters']))) + fields.extend(re.findall(r'(\w+\.\w+)', + str(row['query.formatted_pivots']))) + fields.extend(re.findall(r'(\w+\.\w+)', + str(row['query.sorts']))) + for field in fields: + s = model + '.' + explore + '.' + field + '.' + str(run_count) + formatted_fields.extend([s]) + + field_name = [] + field_use_count = [] + for row in formatted_fields: + field = '.'.join(row.split('.')[:-1]) # remove the count + field_name.append(field) # model.explore.view scoped + count = int(row.split('.')[-1]) + field_use_count.append({ + 'field_name': field, + 'count': count + }) + + c = Counter() + + for value in field_use_count: + c[value['field_name']] += value['count'] + self.fetch_logger.info('Fetch Complete :: Exposed Explore Fields ') + return dict(c) + + def get_used_explores(self, model=None, explore=None, + timeframe=90, min_queries=0): + self.fetch_logger.info('Fetching used explores, %s', locals()) + timeframe = str(timeframe) + ' days' + min_queries = '>=' + str(min_queries) + m = model.replace('_', '^_') + ',' if model is not None else '' + body = { + "model": "i__looker", + "view": "history", + "fields": ["query.view", "history.query_run_count"], + "filters": {"history.created_date": timeframe, + "query.model": m, + "history.query_run_count": min_queries, + "query.view": explore + }, + "limit": "50000" + } + + response = self.looker.run_inline_query("json", body) + + x = {} + for r in response: + x[r['query.view']] = r['history.query_run_count'] + self.fetch_logger.info('Fetch Complete :: Used Explores') + return(x) + + def test_git_connection(self, project): + # enter dev mode + self.looker.update_session(mode='dev') + # obtain tests available + tests = [] + for test in self.looker.git_connection_tests(project_id=project): + tests.append(test['id']) + verbose_result = [] + fail_flag = 0 + for idx, test in enumerate(tests): + r = self.looker.run_git_connection_test(project_id=project, + test_id=test) + verbose_result.append(r['id'] + ' (' + r['status'] + ')') + if r['status'] != 'pass': + fail_flag = 1 + verbose_result = ('\n').join(verbose_result) + result = verbose_result if fail_flag == 1 else 'OK' + return result diff --git a/henry/modules/lookerapi.py b/henry/modules/lookerapi.py new file mode 100644 index 0000000..eff7370 --- /dev/null +++ b/henry/modules/lookerapi.py @@ -0,0 +1,343 @@ +# -*- coding: UTF-8 -*- +import requests +import json +import sys +import logging +import logging.config +from requests.packages.urllib3.exceptions import InsecureRequestWarning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) +logging.getLogger("urllib3").setLevel(logging.WARNING) + + +class LookerApi(object): + def __init__(self, id, secret, host, port, timeout, session_info): + self.api_logger = logging.getLogger('lookerapi') + self.id = id + self.secret = secret + self.host = host + self.port = port + self.timeout = timeout + self.session_info = session_info + + self.session = requests.Session() + self.session.verify = False + + self.auth() + + def auth(self): + self.api_logger.info('Authenticating') + url = f'https://{self.host}:{self.port}/api/3.0/login' + params = {'client_id': self.id, 'client_secret': self.secret} + self.api_logger.info('Request to %s => POST /api/3.0/login, %s', + self.host, {'client_id': params['client_id'], + 'client_secret': "[FILTERED]"}) + r = self.session.post(url, params=params, timeout=self.timeout) + access_token = r.json().get('access_token') + self.session.headers.update({'Authorization': f'Bearer {access_token}', + 'User-Agent': self.session_info}) + if r.status_code == requests.codes.ok: + self.api_logger.info('Request Complete: %s', r.status_code) + else: + self.api_logger.warning('Request Complete: %s', r.status_code) + print('Authentication Error: Check supplied credentials.') + sys.exit(1) + + return + +# GET /lookml_models/ + def get_models(self, fields={}): + url = 'https://{}:{}/api/3.0/{}'.format(self.host, + self.port, + 'lookml_models') + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/lookml_models, %s', + self.host, + params) + r = self.session.get(url, params=params, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.error('Request Complete: %s', r.status_code) + raise(e) + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET /lookml_models/{{NAME}} + def get_model(self, model_name=None, fields={}): + url = 'https://{}:{}/api/3.0/{}/{}'.format(self.host, + self.port, + 'lookml_models', + model_name) + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/lookml_models/%s,' + ' %s', self.host, model_name, params) + r = self.session.get(url, params=params, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.error('Request Complete: %s', r.status_code) + raise(e) + self.api_logger.info('Request Complete: %s', r.status_code) + return [r.json()] + +# GET /lookml_models/{{NAME}}/explores/{{NAME}} + def get_explore(self, model_name=None, explore_name=None, fields={}): + url = 'https://{}:{}/api/3.0/{}/{}/{}/{}'.format(self.host, + self.port, + 'lookml_models', + model_name, + 'explores', + explore_name) + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/lookml_models/%s' + '/explores/%s, %s', self.host, model_name, + explore_name, params) + r = self.session.get(url, params=params, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.error('Request Complete: %s', r.status_code) + return [] + self.api_logger.info('Request Complete: %s', r.status_code) + return [r.json()] + +# GET /projects + def get_projects(self, fields={}): + url = 'https://{}:{}/api/3.0/{}'.format(self.host, + self.port, + 'projects') + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/projects, %s', + self.host, + params) + r = self.session.get(url, params=params, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + raise(e) + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET /projects/{project_id} + def get_project(self, project_id=None, fields={}): + url = 'https://{}:{}/api/3.0/{}/{}'.format(self.host, + self.port, + 'projects', + project_id) + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/projects/%s, %s', + self.host, + project_id, + params) + r = self.session.get(url, params=params, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.error('Request Complete: %s', r.status_code) + raise e + self.api_logger.info('Request Complete: %s', r.status_code) + return [r.json()] + +# GET /projects/{project_id}/files + def get_project_files(self, project=None, fields={}): + url = 'https://{}:{}/api/3.0/{}/{}/{}'.format(self.host, + self.port, + 'projects', + project, + 'files') + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/projects/%s/files,' + ' %s', self.host, project, params) + r = self.session.get(url, params=params, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + print('Project not found: %s', project) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# POST /queries/run/{result_format} + def run_inline_query(self, result_format, body, fields={}): + url = 'https://{}:{}/api/3.0/{}/{}/{}'.format(self.host, + self.port, + 'queries', + 'run', + result_format) + params = fields + self.api_logger.info('Request to %s => POST /api/3.0/queries/run/%s, ' + '%s', self.host, result_format, params) + self.api_logger.info('Query params=%s', body) + r = self.session.post(url, json.dumps(body), params=params, + timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + print("Error: " + str(e)) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# PATCH session + def update_session(self, mode): + url = 'https://{}:{}/api/3.0/{}'.format(self.host, + self.port, + 'session') + body = {'workspace_id': str(mode)} + self.api_logger.info('Request to %s => PATCH /api/3.0/session, %s', + self.host, + body) + r = self.session.patch(url, json=body, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + print("Error: " + str(e)) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET session + def get_session(self, fields={}): + url = 'https://{}:{}/api/3.0/{}'.format(self.host, + self.port, + 'session') + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/session, %s', + self.host, + params) + r = self.session.get(url, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + print("Error: " + str(e)) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET /projects/{project_id}/git_connection_tests + def git_connection_tests(self, project_id, fields={}): + url = ('https://{}:{}/api/3.0/projects/{}/' + 'git_connection_tests').format(self.host, self.port, project_id) + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/projects/%s/' + 'git_connection_tests, %s', + self.host, + project_id, + params) + r = self.session.get(url, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + print("Error: " + str(e)) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET /projects/{project_id}/git_connection_tests/{test_id} + def run_git_connection_test(self, project_id, test_id, fields={}): + url = ('https://{}:{}/api/3.0/projects/{}/git_connection_tests/' + '{}').format(self.host, self.port, project_id, test_id) + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/projects/%s/' + 'git_connection_tests/%s, %s', + self.host, + project_id, + test_id, + params) + r = self.session.get(url, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + print("Error: " + str(e)) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET /connections + def get_connections(self, fields={}): + url = 'https://{}:{}/api/3.0/connections'.format(self.host, self.port) + params = fields + self.api_logger.info('Request to %s => GET /api/3.0/connections, %s', + self.host, + params) + r = self.session.get(url, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# PUT /connections/{connection_name}/test + def test_connection(self, connection, fields={}): + url = 'https://{}:{}/api/3.0/connections/{}/test'.format(self.host, + self.port, + connection) + params = fields + self.api_logger.info('Request to %s => POST /api/3.0/connections/' + '%s/test, %s', self.host, connection, params) + r = self.session.put(url, params=params, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET /legacy_features + def get_legacy_features(self, fields={}): + url = 'https://{}:{}/api/3.0/legacy_features'.format(self.host, + self.port) + params = fields + self.api_logger.info('Request to %s => POST /api/3.0/legacy_features,' + ' %s', self.host, params) + r = self.session.get(url, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET /integrations + def get_integrations(self, fields={}): + url = 'https://{}:{}/api/3.0/integrations'.format(self.host, self.port) + params = fields + self.api_logger.info('Request to %s => POST /api/3.0/integrations, %s', + self.host, + params) + r = self.session.get(url, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() + +# GET /versions + def get_version(self, fields={}): + url = 'https://{}:{}/api/3.0/versions'.format(self.host, self.port) + params = fields + self.api_logger.info('Request to %s => POST /api/3.0/versions, %s', + self.host, + params) + r = self.session.get(url, timeout=self.timeout) + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + self.api_logger.warning('Request Complete: %s', r.status_code) + print("Error: " + str(e)) + return + self.api_logger.info('Request Complete: %s', r.status_code) + return r.json() diff --git a/henry/modules/spinner.py b/henry/modules/spinner.py new file mode 100644 index 0000000..900bf25 --- /dev/null +++ b/henry/modules/spinner.py @@ -0,0 +1,32 @@ +import threading +import sys +import time + + +class SpinnerThread(threading.Thread): + + def __init__(self): + super().__init__(target=self._spin) + self._stopevent = threading.Event() + + def stop(self): + sys.stdout.write('\b') + self._stopevent.set() + + def _spin(self): + + while not self._stopevent.isSet(): + for t in '|/-\\': + sys.stdout.write(t) + sys.stdout.flush() + time.sleep(0.1) + sys.stdout.write('\b') + + +class Spinner(object): + def __enter__(self): + self.spinner = SpinnerThread() + self.spinner.start() + + def __exit__(self, exc_type, exc_value, tb): + self.spinner.stop() diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..00d614c --- /dev/null +++ b/readme.md @@ -0,0 +1,296 @@ +![image](https://github.com/looker-open-source/henry/blob/master/doc/logo/logo.png?raw=true) + +----------------- +# Henry: A Looker Cleanup Tool +Henry is a command line tool that helps determine model bloat in your Looker instance and identify unused content in models and explores. It is meant to help developers cleanup models from unused explores and explores from unused joins and fields, as well as maintain a healthy and user-friendly instance. + +## Table of Contents +- [Henry: A Looker Cleanup Tool](#henry-a-looker-cleanup-tool) + - [Table of Contents](#table-of-contents) + - [Status and Support](#status-and-support) + - [Where to get it](#where-to-get-it) + - [Usage](#usage) + - [Storing Credentials](#storing-credentials) + - [Global Config File](#global-config-file) + - [API timeout settings](#api-timeout-settings) + - [Config Path](#config-path) + - [Global Options that apply to many commands](#global-options-that-apply-to-many-commands) + - [Suppressing Formatted Output](#suppressing-formatted-output) + - [Output to File](#output-to-file) + - [Pulse Command](#pulse-command) + - [Connection Checks](#connection-checks) + - [Query Stats](#query-stats) + - [Scheduled Plans](#scheduled-plans) + - [Legacy Features](#legacy-features) + - [Version](#version) + - [Analyze Command](#analyze-command) + - [analyze projects](#analyze-projects) + - [analyze models](#analyze-models) + - [analyze explores](#analyze-explores) + - [Vacuum Information](#vacuum-information) + - [vacuum models](#vacuum-models) + - [vacuum explores](#vacuum-explores) + - [Logging](#logging) + - [Dependencies](#dependencies) + - [Development](#development) + - [Authors](#authors) + - [Contributing](#contributing) + - [Code of Conduct](#code-of-conduct) + - [Copyright](#copyright) + + +## Status and Support +Henry is **NOT** supported or warranted by Looker in any way. Please do not contact Looker support +for issues with Henry. Issues can be logged via https://github.com/looker-open-source/henry/issues + + +## Where to get it +The source code is currently hosted on GitHub at https://github.com/looker-open-source/henry/. The latest released version can be found on [PyPI](https://pypi.org/project/henry/) and can be installed using: + + $ pip install henry + +For development setup, follow the Development setup [below](#development). + + +## Usage +In order to display usage information, use: + + $ henry --help + + +### Storing Credentials +API3 login credentials can be specified at runtime using various flags or more conveniently, using a `config.yml` having the format shown below. + +``` +hosts: + dev_looker: + host: devhostname.looker.com + id: AbCdEfGhIjKlMnOp + secret: QrStUvWxYz1234567890 + staging_looker: + host: staginghostname.looker.com + id: AbCdEfGhIjKlMnOp + secret: QrStUvWxYz1234567890 +``` + +Make sure that the `config.yml` file has restricted permissions by running `chmod 600 config.yml`. The tool will also ensure that this is the case every time it writes to the file. + +If `config.yml` resides in the current working directory, then you don't need to do anything. If not, its location needs to be specified at runtime using the `--path` parameter or in the [global config file](#global-config-file). + + +### Global Config File +A global settings file called `settings.json` can be defined in `~/.henry`. The file can be used to define a number of paramaters to be used at runtime: + +``` +{ + "api_conn_timeout": x, + "config_path": "/path/to/api3/credentials/yml/file" + +} +``` + +#### API timeout settings +The `api_conn_timeout` parameter can be used to specify API call timeout settings. It can take 3 types of values: null, an integer representing +connect and read timeouts (in seconds) combined or a list that specifies +the connect and read timeouts separately (e.g. "[5, 15]"). + + +#### Config Path +The `config_path` parameter defines the absolute location to the [API3 credentials file](#storing-credentials). + +In order of precedence, these are the ways that are used to define the location of the credentials file path: +--path, config_path in ~/.henry/settings.json and then the default. + + +### Global Options that apply to many commands + +#### Suppressing Formatted Output +Many commands provide tabular output. For tables the option `--plain` will suppress the table headers and format lines, making it easier to use tools like grep, awk, etc. to retrieve values from the output of these commands. + + +#### Output to File +Using the `--output` option allows you to specify a path and a file to save the results to. When combined with `--plain` the format lines will be suppressed. Example usage: + + $ henry vacuum models --plain --output=unused_explores.csv + +saves the results to *unused_explores.csv* in the current working directory. + + +### Pulse Command +The command `henry pulse` runs a number of tests that help determine the overall instance health. A healthy Looker instance should pass all the tests. Below is a list of tests currently implemented. + +#### Connection Checks +Runs specific tests for each connection to make sure the connection is in working order. If any tests fail, the output will show which tests passed or failed for that particular connection. Example: +``` ++------------------+------------------------------------------------------------------------------------------------------+ +| Connection | Status | +|------------------+------------------------------------------------------------------------------------------------------| +| thelook | -- Failed to create or write to pdt connection registration table tmp.connection_reg_r3 : Connection | +| | registration error for thelook: max registrations reached for connection thelook | +| assets_analytics | OK | +| events_ecommerce | OK | ++------------------+------------------------------------------------------------------------------------------------------+ +``` + +#### Query Stats +Checks how many queries were run over the past 30 days and how many of them errored or got killed as well as some statistics around runtimes times. The IDs of queries that took more than 5 times the average query runtime are also outputted. + +#### Scheduled Plans +Determines the number of scheduled jobs that ran in the past 30 days, how many were successful, how many ran but did not deliver or failed to run altogether. + +#### Legacy Features +Outputs a list of legacy features that are still in use if any. These are features that have been replaced with improved ones and should be moved away from. + +#### Version +Checks if the latest Looker version is being used. Looker supports only up to 3 releases back. + + +### Analyze Command +The `analyze` command is meant to help identify models and explores that have become bloated and use `vacuum` on them in order to trim them. + + +#### analyze projects +The `analyze projects` command scans projects for their content as well as checks for the status of quintessential features for success such as the git connection status and validation requirements. +``` ++-------------------+---------------+--------------+-------------------------+---------------------+-----------------------+ +| project | model_count | view_count | git_connection_status | pull_request_mode | validation_required | +|-------------------+---------------+--------------+-------------------------+---------------------+-----------------------| +| marketing | 1 | 13 | OK | links | True | +| admin | 2 | 74 | OK | off | True | +| powered_by_looker | 1 | 14 | OK | links | True | +| salesforce | 1 | 36 | OK | required | False | +| thelook_event | 1 | 17 | OK | required | True | ++-------------------+---------------+--------------+-------------------------+---------------------+-----------------------+ +``` + + +#### analyze models +Shows the number of explores in each model as well as the number of queries against that model. +``` ++-------------------+------------------+-----------------+-------------------+-------------------+ +| project | model | explore_count | unused_explores | query_run_count | +|-------------------+------------------+-----------------+-------------------+-------------------| +| salesforce | salesforce | 8 | 0 | 39923 | +| thelook_event | thelook | 10 | 0 | 166307 | +| powered_by_looker | powered_by | 5 | 0 | 49122 | +| marketing | thelook_adwords | 3 | 0 | 40869 | +| admin | looker_base | 0 | 0 | 0 | +| admin | looker_on_looker | 10 | 9 | 28 | ++-------------------+------------------+-----------------+-------------------+-------------------+ +``` + + +#### analyze explores +Shows explores and their usage. If the `--min_queries` argument is passed, joins and fields that have been used less than the threshold specified will be considered as unused. +``` ++---------+-----------------------------------------+-------------+-------------------+--------------+----------------+---------------+-----------------+---------------+ +| model | explore | is_hidden | has_description | join_count | unused_joins | field_count | unused_fields | query_count | +|---------+-----------------------------------------+-------------+-------------------+--------------+----------------+---------------+-----------------+---------------| +| thelook | cohorts | True | No | 3 | 0 | 19 | 4 | 333 | +| thelook | data_tool | True | No | 3 | 0 | 111 | 90 | 736 | +| thelook | order_items | False | No | 7 | 0 | 153 | 16 | 126898 | +| thelook | events | False | No | 6 | 0 | 167 | 68 | 19372 | +| thelook | sessions | False | No | 6 | 0 | 167 | 83 | 12205 | +| thelook | affinity | False | No | 2 | 0 | 34 | 13 | 3179 | +| thelook | orders_with_share_of_wallet_application | False | No | 9 | 0 | 161 | 140 | 1586 | +| thelook | journey_mapping | False | No | 11 | 2 | 238 | 228 | 14 | +| thelook | inventory_snapshot | False | No | 3 | 0 | 25 | 15 | 33 | +| thelook | kitten_order_items | True | No | 8 | 0 | 154 | 138 | 39 | ++---------+-----------------------------------------+-------------+-------------------+--------------+----------------+---------------+-----------------+---------------+ +``` + + +### Vacuum Information +The `vacuum` command outputs a list of unused content based on predefined criteria that a developer can then use to cleanup models and explores. + + +#### vacuum models +The `vacuum models` command exposes models and the number of queries against them over a predefined period of time. Explores that are listed here have not had the minimum number of queries against them in the timeframe specified. As a result it is safe to hide them and later delete them. +``` ++------------------+---------------------------------------------+-------------------------+ +| model | unused_explores | model_query_run_count | +|------------------+---------------------------------------------+-------------------------| +| salesforce | None | 39450 | +| thelook | None | 164930 | +| powered_by | None | 49453 | +| thelook_adwords | None | 38108 | +| looker_on_looker | user_full | 27 | +| | history_full | | +| | content_view | | +| | project_status | | +| | field_usage_full | | +| | dashboard_performance_full | | +| | user_weekly_app_activity_period_over_period | | +| | pdt_state | | +| | user_daily_query_activity | | ++------------------+---------------------------------------------+-------------------------+ +``` + + +#### vacuum explores +The `vacuum explores` command exposes joins and exposes fields that are below the minimum number of queries threshold (default =0, can be changed using the `--min_queries` argument) over the specified timeframe (default: 90, can be changed using the `--timeframe` argument). + +Example: from the analyze function run [above](#analyze_explores), we know that the cohorts explore has 4 fields that haven't been queried once in the past 90 days. Running the following vacuum command: + + $ henry vacuum explores --model thelook --explore cohorts + + provides the name of the unused fields: +``` ++---------+-----------+----------------+------------------------------+ +| model | explore | unused_joins | unused_fields | +|---------+-----------+----------------+------------------------------| +| thelook | cohorts | N/A | order_items.created_date | +| | | | order_items.id | +| | | | order_items.total_sale_price | +| | | | users.gender | ++---------+-----------+----------------+------------------------------+ +``` +It is very important to note that fields vacuumed fields in one explore are not meant to be completely removed from view files altogether because they might be used in other explores or joins. Instead, one should either hide those fields (if they're not used anywhere else) or exclude them from the explore using the _fields_ LookML parameter. + + +## Logging +The tool logs activity as it's being used. Log files are stored in `~/.henry/log/` in your home directory. Sensitive information such as your client secret is filtered out for security reasons. Moreover, log files have restricted permissions which allow only the owner to read and write. + +The logging module utilises a rotating file handler which is currently set to rollover when the current log file reaches 500 KB in size. The system saves old log files by adding the suffix '.1', '.2' etc., to the filename. The file being written to is always named `henry.log`. No more than 10 log files are kept at any point in time, ensuring logs do not consume more than 5 MB max. + + +## Dependencies +- [PyYAML](https://pyyaml.org/): 3.12 or higher +- [requests](http://docs.python-requests.org/en/master/): 2.18.4 or higher +- [tabulate](https://bitbucket.org/astanin/python-tabulate): 0.8.2 or higher +- [tqdm](https://tqdm.github.io/): 4.23.4 or higher + + +## Development + +To install henry in development mode you need to install the dependencies above and clone the project's repo with: + + $ git clone git@github.com:looker-open-source/henry.git + +You can then install using: + + $ python setup.py develop + +Alternatively, you can use `pip` if you want all the dependencies pulled in automatically (the -e option is for installing it in [development mode](https://pip.pypa.io/en/latest/reference/pip_install/#editable-installs)). + + $ pip install -e . + + +## Authors + +Henry has primarily been developed by [Joseph Axisa](https://github.com/josephaxisa). See [all contributors](https://github.com/looker-open-source/henry/graphs/contributors). + + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/looker-open-source/henry/issues. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. + + +## Code of Conduct + +Everyone interacting in the Henry project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/looker-open-source/henry/blob/master/CODE_OF_CONDUCT.md). + + +## Copyright + +Copyright (c) 2018 Joseph Axisa for Looker Data Sciences. See [MIT License](LICENSE.txt) for further details. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fb3d8b0 --- /dev/null +++ b/setup.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import io +import os +import sys +from shutil import rmtree +from setuptools import find_packages, setup, Command + +# Package meta-data. +NAME = 'henry' +DESCRIPTION = 'A Looker Cleanup Tool' +URL = "https://github.com/looker-open-source/henry" +EMAIL = 'jax@looker.com' +AUTHOR = 'Joseph Axisa' +REQUIRES_PYTHON = '>=3.6' +VERSION = '' + +# What packages are required for this module to be executed? +REQUIRED = [ + 'requests', 'pyyaml', 'tabulate', 'requests', 'tqdm' +] + +# What packages are optional? +EXTRAS = { + # 'fancy feature': ['django'], +} + +here = os.path.abspath(os.path.dirname(__file__)) + +try: + with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = '\n' + f.read() +except FileNotFoundError: + long_description = DESCRIPTION + +# Load the package's __version__.py module as a dictionary. +about = {} +if not VERSION: + with open(os.path.join(here, NAME, '__version__.py')) as f: + exec(f.read(), about) +else: + about['__version__'] = VERSION + +if not (sys.version_info.major == 3 and sys.version_info.minor >= 6): + sys.exit("Sorry, Henry requires Python 3.6 or later.") + + +class UploadCommand(Command): + """Support setup.py upload.""" + + description = 'Build and publish the package.' + user_options = [] + + @staticmethod + def status(s): + """Prints things in bold.""" + print('\033[1m{0}\033[0m'.format(s)) + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + try: + self.status('Removing previous builds…') + rmtree(os.path.join(here, 'dist')) + except OSError: + pass + + self.status('Building Source and Wheel distribution…') + os.system('{0} setup.py sdist bdist_wheel'.format( + sys.executable)) + + self.status('Uploading the package to PyPI via Twine…') + os.system('twine upload dist/*') + + self.status('Pushing git tags…') + os.system('git tag v{0}'.format(about['__version__'])) + os.system('git push --tags') + + sys.exit() + + +setup( + name=NAME, + version=about['__version__'], + description=DESCRIPTION, + long_description=long_description, + long_description_content_type='text/markdown', + author=AUTHOR, + author_email=EMAIL, + python_requires=REQUIRES_PYTHON, + url=URL, + packages=['henry', 'henry/commands', 'henry/modules'], + data_files=[('henry/.support_files/', + ['henry/.support_files/logging.conf']), + ('henry/.support_files/', ['henry/.support_files/help.rtf'])], + entry_points={ + 'console_scripts': ['henry=henry.cli:main'], + }, + install_requires=REQUIRED, + extras_require=EXTRAS, + include_package_data=True, + license='MIT', + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + cmdclass={ + 'upload': UploadCommand, + }, +)