From c39cd3aefa263eef9d2a4f42717e0eb05d3e0920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Deuchnord?= Date: Thu, 4 Jun 2020 13:49:37 +0200 Subject: [PATCH] fix: handle out of range date error --- .github/workflows/unit-tests.yml | 2 +- .scripts/tests-e2e.sh | 2 + CONTRIBUTING.md | 2 +- Makefile | 8 + Pipfile | 2 +- Pipfile.lock | 2 +- kosmorrolib/assets/moonphases/png/unknown.png | Bin 0 -> 26900 bytes kosmorrolib/assets/moonphases/svg/unknown.svg | 31 ++++ kosmorrolib/data.py | 5 +- kosmorrolib/dumper.py | 18 ++- kosmorrolib/ephemerides.py | 82 +++++++---- kosmorrolib/events.py | 21 ++- kosmorrolib/exceptions.py | 13 ++ kosmorrolib/i18n.py | 7 + kosmorrolib/locales/messages.pot | 138 ++++++++++-------- kosmorrolib/main.py | 58 +++++--- setup.py | 2 +- test/dumper.py | 4 + test/ephemerides.py | 9 ++ test/events.py | 5 + 20 files changed, 280 insertions(+), 131 deletions(-) create mode 100644 kosmorrolib/assets/moonphases/png/unknown.png create mode 100644 kosmorrolib/assets/moonphases/svg/unknown.svg diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 48872fd..24bc50b 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -20,5 +20,5 @@ jobs: env: COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} run: | - pipenv run python -m coverage run -m unittest test + make test COVERALLS_REPO_TOKEN=$COVERALLS_TOKEN pipenv run coveralls diff --git a/.scripts/tests-e2e.sh b/.scripts/tests-e2e.sh index b64e84a..3c35f9f 100755 --- a/.scripts/tests-e2e.sh +++ b/.scripts/tests-e2e.sh @@ -81,6 +81,8 @@ assertSuccess "kosmorro -h" assertSuccess "kosmorro -d 2020-01-27" assertFailure "kosmorro -d yolo-yo-lo" assertFailure "kosmorro -d 2020-13-32" +assertFailure "kosmorro --date=1789-05-05" +assertFailure "kosmorro --date=3000-01-01" assertSuccess "kosmorro --date='+3y 5m3d'" assertSuccess "kosmorro --date='-1y3d'" assertFailure "kosmorro --date='+3d4m" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76a790e..4806fe9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,7 +80,7 @@ Kosmorro's unit tests use Python's official `unittest` module. They live in the You can also run them by invoking the following command: ```shell -pipenv run python -m unittest test +make test ``` Note: there are currently some memory leaks in the unit tests, making the result quite difficult to read. I am working to fix this. diff --git a/Makefile b/Makefile index 00398ba..d6ed8b7 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,11 @@ +.PHONY: test + +test: + unset KOSMORRO_LATITUDE; \ + unset KOSMORRO_LONGITUDE; \ + unset KOSMORRO_TIMEZONE; \ + LANG=C pipenv run python3 -m coverage run -m unittest test + build: ronn --roff manpage/kosmorro.1.md ronn --roff manpage/kosmorro.7.md diff --git a/Pipfile b/Pipfile index 0074f6c..b0a4c55 100644 --- a/Pipfile +++ b/Pipfile @@ -11,7 +11,7 @@ unittest-data-provider = "*" coveralls = "*" [packages] -skyfield = ">=1.13.0,<2.0.0" +skyfield = ">=1.21.0,<2.0.0" tabulate = "*" numpy = ">=1.17.0,<2.0.0" termcolor = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 84b2b70..7371fe8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "afd17771258a86b04cab79d21d2afbfb1faf44bbe6e760da9140e72b0999a5b1" + "sha256": "9ed5ee6bbfde75ee77c89fdc09a793f5c00f9782968dc310e1eb8d3386378d9e" }, "pipfile-spec": 6, "requires": { diff --git a/kosmorrolib/assets/moonphases/png/unknown.png b/kosmorrolib/assets/moonphases/png/unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..deff19ab2e6bbf0b38dd4823005c129ba8209980 GIT binary patch literal 26900 zcmXtfWl$Vl)AlazEbi`3a0|iR3A#XVXK{B465QQ2LBa+Pwpefp?(P!YA^EtU`raQ? zJ3BQsUFV#(tNZF04K;a8G*UDG0D!5eAfp8U0EPa$P>|l9Oa`CZzx|-PC>Xc{09bwh zUBE%-D$BQrBp$N*9@@^<9^PhdRse5rZ%#XigS&;9ixsD{n@#Q?F;W148lWg6`Ozou zw9~hdV$hfBq599W?x~LtwE!ITMST)=YX~1pat8abp(E8GrEqfQkbJV$Fz>{e{)D-@ z8)HJLB~NK8iIpQ0bbAr%$V=x?!>q;be~RZ&%%F+Dkc=Q{J_s*y3WKN2Xjw0B-%tG0 z)snf~{?mCQ>UpQFzuj18<9_YWw@<;JY7qi&iX)hiLY6or+<7(x;{q0=P#`?6>+j|IJb;v{5fCn-_5I!u2lWev+3-Lls zq8lUWAGS~+bOq@)89OlSM70$*F$loLSq6HX7>FRafPzpYY)#RMd-#O_wu2%{fG9XV z1}655hfG{pWxhK4lH0&GhQvP%mu?J<0C=J4B%-5Ir9(WUa~Psm2LUw|U}zFR%wAqG*;TZu=BJoi?fZaU zVt6Ub>cH0T%p6YJMTbdxlXD4Kf~dGJ351x@lbjIX1Mkgn-PK_mYk;%qU~0%U4j>r4 z`F#`Vk(eiynSw7i_0Kn4{szi2o!$`#hFJ@tZ6<~kZy+Y9fRh*HId4$wP8J^#HecW*{t1NawaMTB zI7Q-{{&~#dFbPrlqB`TiGJyvl08HIEU_+{3hk~JXrqA3O&jtW0&0&EJO%<*$`? zc)yuB%gqYQF2@7mVa{+qSVG1vRwh*Thk`L$Pz!?1AmXC#FjJ>W@nsXk#p2oEuz~Oj zaqCvB&6$vE=g?F^c7ypSgx7sdG_P|&M_ z=B{gO#%+&8;U?NZ-5}saJLIoW?=boZHDER9(7;Y(_vrgI@QDHT55eWGUZ?%ZwsgY- zt)~td_>>3?0R8OWzmuLUkdg7gKV%=&fJ3}E2)R}dN&QY>vDz6M#QWpRdu+hp4{(Q* zbcZb3(i>o~2>1PQ0N@kgB^WMG*lwl9pWt=h64Ac}e*6>i(4>@&wgOXL!1B2sIOn1- zkpu`9+s`Q066SM9{LWfJSQ|? zcn`Sc{+1|&!hH!WdaMKlVSF>mCF3BJFI@e@Ad^dY_}-m}S{?qT54KFQIbVIKPCPHy zoo#1vuO0G_46y#qs7&2Hu-|O|cPGX}H%8v0E{y<(=1p!M&z&ROi#qV8vdC}asee8S z>a0asCrZs-=2B_!>#$`2~SzJ;RdOu1Gaa_#;)2U5Iw--?A& zf3gD*M>dV+YYE@{V;5EGOKM5!4EgG(t$gbpY9a1TviNH%*FE&e*;J9YTg30-Xr=!S zQ{s#l5TuYZ=ctytCT-ZdZuHOfxHw8EMC!mX<~wjADkL@Y3vZR+lrzraEgs+%8}O^l zU&_O5wgEm8a1w(~ngp;Fu|D4e5{zF13(tzQ}%ulChP84mQOEyi(o{;G@Qy zVWE3npL^sLwEFy}UqJ zJQkje3XCE1!xq=!(AY2ZLd-<;bcP_o~&ml4fQNSvJaVYL`*UNsP ztF&~ea49FLx#UowF7Chn6gPBE^xv`iCVP>90D!(IoJz3q*M3~s`)TsK1Yqq4qzg*B zd9i*Zd=-ey1;@`RCgJM({j+o!)A9Q1bx+Rm@X6<8ZQv@rs@XOmgbQwwbu2@W9+5pY zH)1S8wpzlC7T#~wwpzr8poM&a|8xTo4+8 z!dQ)mOC8sp7V9#tO8GsXRT8D=hZP+$3CXXa3+~7$VOv`?7+6>^R}}juQWP3u z;zP&E1$*C%K&o+T^fWELTn@fm)v-w~b2Bp>^w`+5?%eST0hRhE+6Xn>@S32g6r0(NF=D&8EcIZl)PHE7B-ST z3UwQBYlW=0L=wl#;kBHPZQh-=x4PRP{dfCk&EO%Z!^->X&W3)2tUn$Nn>S(v^5zcg zDA-2b)w3QSH6bD#wVQL)iZG=DU1MOXkzQ(Nq+#LlJr7Tur-cKzco*T$jzj&M2sSq- z*geaii7|6~d(wrv4Ol!`%amg?v9L0&xW$hK%TJ7!dpH!Y9Hv+2J-hfZXcjRzt9NEa zcgTPsr7R9o1|6hxf#D(5aK)z_?&eaXhEi27i z(ui&>GmM0zK>?RKhlj&2{_uqtfRGP9JP_MAmA@;k`fa;?m44FdbHrL_+@6hQBU*@YucMZmb~^zA0MTiowt%q-R9Z@xwb+Ka((KfG;o|dTmbh3TfwkTy z&L6K`Ik7b|$D)YhU;vfKTi3+Qx!SwlQ-ix%gY6=|-2QWJrE?==!Z)Zfp@UJ@o36^OYesv`y&Bec^M zfA;aTr@yyWaI0)msRH>Gqq*1nb8QAA!mJ^Le@w%WpaTtBYECE6cA+@42bYVb6KB>T`AW4`owBJ2!TB!g*>+!;O<%<)(+q#gR$2+ zsEKjb8vK#H&etbUWDTldn^NRszx3dS2V9P}`8%$Tab{02V>1?MMA>ARnA+a*v!aD& zqhqhRx&D4Nzy_c`CbwJ_a#i^2c7B@K(;Fm8jgp~!ma~|EOqHX+9>wE#+AAo5GxaB6 zBttJuWO(ibv*T{rHLu&nfVqo}jo2~byIV>-QtO8)-skqyz-prGYsmR~LK+ zbh9{P{b4_+BzD=~r{8YfB+GzyuYwCl{+;1zAJ_CHZ#vTL86OQKL&@_<3bD zbYJ~C@JnA*;?52IU^bNW*Z%c!V~pCL;ZCgCbyV_hJBa>el~9ip2{5u7YyYa+Ma0X4 z8iS}TU)A@yh_gu~+ncfX&MEiuVdr`C=K$x|>7R*WQ)oVp?2W;h1y0UgzNlQ?ZuD)Z zv2bZ#$F|RiE2@)fnTNad_q6p7;jjTj-taH-=|sdLu!jRc@8V}cLpf?%!LY-!FqtGn zC=e0%nVJ?mrIj_}90POP2ak$4x5}tw!Z8?pi)DWY>cM(FVcfw`@yD%hVAlwyG*VrKNp~9Y^9u zodTcbN_zrQvi&o1PnD=egoocF)Q})TUemj121-j1TCRxzS6oYWVs3aKvi9cB+h3eL z6uHLprndE)>=D=h-c{BI7!QcAPZPg<(Ak?J9 zp56q)ssdT98FS^v5QJ@8_J`J@nnOS zuan|2#XXX2S1QFSZt3xgqK9HkisBcC^bfk#ZU6bzI%9MNPQO3-hk9EHCJTY9%)Ut||!<*OV+|6)k2 zo0Nw;vdXwT&%@oGOY$WzVD9gJj9DC^ZbHEmZP|hjQnJc;K}JhA3JLpHieNlf->8o| z&oR=<2rpi)xy*vZ1%_;Q1}tT~GNKdUE+T$Rz)#@8hRL4L&Pwyl1ck|q{u-bR zW16ObiKH~G3k`eZPFJD)f^Fq}Y<&e}0<`Mm>VZmbeQ2*vJXR9*@}_8|baX!Z+3>?_ zNi0b#Jf#(c0M%g38BhuJsy-&9mnlW*@Czh4$irRXsx$e^jfpB#@_!|>!vV0h6_JtK115b)5J@cg;-D)&0j(9eU1{p-ZxvYT^f}8IA9S@J*x{9ArHM$_pYhTEw)qex3{<3KX3D z*(OTcIAF_P72~S+9x>VCqoNQrlo-rKWu@UWx>j3RjQ;ZtD{9#@Py#Bq_=05*>_;HOC zx0uZ7w< zK`qgMxwP7kh8jBquddP||Ds@cFONKgnz%j2F9ZPOMLT*AhwGL<5FD}(pVjRzcLSfj z$`fu+Fw0h&Yt_}IAEOXBC%OXLb$CW^QMnw}mp*!>*6Z+m_@lWAVJWspMQE`g3i-5C zjwBf7$u!$Hg9R;iG1tDHpGB`T>BL=Wv5U(gjAI395;)eweZF7)>FH1(6rfUIpp$v= zL-_ZwvYSEz`V*>YrDYkzhc8}aD4Ri~MRa2yk$c<{1JJhrv0<3&8Gm5u{rl>#w6;fQ+~~M%GZx!Mez-#FE|J@1MB6GXmK>g)_f|w z#2jl11-1942W(l39vk{-pK%vk&B*U-Z6>pzp%6czG_BPRU$TYDig)~;cr!EJVekfg z&^MVi#tmpYCxi8_eL|6uH(u{1h{*6G(lp<2!na>wT9$;g9A^~1CnhCiFxilwf&cVHJ zo$p49T7_FkSBL!ma0J$BT&PfW6dr4JeigBBYj`@z?!DpDP;z~|;#W~C7`$bfaSz!O zVF@7>IISg;ElKgsB|7oPRLiGe1@MvotdtF0kP29!v zWeEfPd7ZqtM8lI~^KT(QysSbW8?p0&X&r47u^@nF(=xr%U2AzA&W{BL9MRJK%2Gq6 zS4f)#wvvuSUaf3q74!Sa^~6?}Ei@Cc2iEm~Yo45OU5Kymlzo%S>yxTc zs}Ex7)~3wZ_K7w56xvVu*yEvQ38jgVK?X1 zY&z0FgqD*&z-#*$J)BS--ibW?2Z5sB0+?1fWR*{Zu-e4;cOdud1Wovqlx*X%iHV)f z9%&)pV|LQ`3eQL1DO*0*t!;v_@$sFm=lOw-Hlfp`f_Tw96aIG^)nJWGb{c79{ zM;e!hIcxM9nZnozH_VQu#G;lb7Mt01O=@j(fHs#-_!=|^aG=J zhR$_>`4s6i%^EjjW9)A`i>q6sPZ+740%uI!D0F;_gheS9!E3{1neeNc_3baGQES-c z<-54J!-VE#Ix#cxlv{3j`6$=pS>Y}X#Lf{!d=E>j zvtG($RrzV%s6vMJWBJ#QSNQc$!khCHI_|Rd3NF=uokPU3JJlq}OPVKhWzR#@9eefC zbgDFF-lFJ#$N6UBxT6{IWeT+tAY8}{#WR;D%KqcEp36nKw5>qyna7~3gu)FfPo-UJ zRU&G@#^4^?0@Q9Ma5**Naf(TF{@nD;2o8XLD^gKWG5493!C`$MtWy}SeFAU*{qXB5TD4j-k|b9Z`(W}@=)oiLr7MGVq{5*k<)b?Q36sND-5+yV(ZS*FyBO{LKEyP)u=n-oTk z0_FK%r!=G54H1<<24h+67320Vf)kZfq(QU$mBeG#vwIu72RcwS=i_Zf>3N& zxAdu&}+xil5kuCmc_08AI0=Le&xPJ+g}nj_%y9Sfuz^hvC#qcV3|zS>6?9G9r9&$dq?f%Eow_WiJ1( z-cwPhZV?cm;*(LqiQAvfbJEl|=(<0K7+6fe)G7p7&H8nB2G*!f_boE=DQ&aNWM~uh zwt2T^5B2YiV- zf3_6>FSfb{vON7zNH!)@OaU9!?6?Jp8`adzuccUWPULE*N`mgUc>-OrfhT_*3eLve zaaVr^QxPZ2nrk`pl)nc5rlAsJ#>*YJqb11f1E==fv(y=7_nv{hxx0cenhe)paehqh zy*!Qz1w6FkylTKokoB-=*4uc)IdTQ{$#wW;N2+6W&m$%e#|#PtoiYy{{oM`SO1yS# zhB#Ep-D<5O>smuF|ak7?+Y31)Es+*WzjaQ=YG0H_D`%-~Q?S}*9IKG}+dP?tvgrf17 z`YAoFqSY2L_IMOz{+zP566s$XY-aR!WV5?D8mRph{~q8q0ghS}nbb&J{h`*7*5j)c;2jT_4n2_Cjb}RG=42H zlL>N7seD5uzm}W4o&?UqH?PVavOZ;_m=^8eISbZOd`zh<_RfbU5V8gZP@W~&i#r*kk z)Me)vUjjug-}E|L>{+&g>(cBvIL@8+o3u|~j4B*TiO%uty{OFK=L$h0f-NQU;MA&v zXq!x?gdu5$ZUo1L_0si_&BG{wqiIeOZHpx>b?&up#)2**bjgm@oQ1c+{o(0TcSPx+ znj&h|I?B^p+-&%rKu`ImuZd$F3HBPD*d4R;m%}Qn61JXPymX=(gHPDA;#C!m@bF=D z-D#ck!jj0FNn`}wucH^sKP4R>8Mw>1m&wcV|FgI1PtO8>*7wd|To0!-**WJ^wPk&1 zjE&yQ9`dtu)IU?pGAEw)<8{o)F!^74M602Wtmk#HYsO{XZ1Z$0`qxXQZhDEsZ*HC5 zP8{7hPR4Jrj6cL=vo#N?X4!@=Ug-tOYT<28C=|P`nso#Ko)Yf(-o8>i31wF?m#_X9uHM}Ee+3~<#(|H>Z>eXwpj>}()0Vm*%zFsNri&|)uvD1R$*<@&p4cSnG$H93)?MUpO} zy1-p{jL$ug&ZDHt)G?33^dS!9-TsVEF}|L<)cokJ;+}ZHx9LPKe_yQL%8yVIn*`zi z;{s6gA%^9r6V%dG!O}p8S-79xxo@3{$=qe*vi`Ia0)nj?b>2Ddy_D@*#nKOMgdSFg zqVBEX^8G2gmgKICOtK7DTuO(KFFmLIG#_@rV_Lp!t4U)QB6Q#fKdj$l?esp5lDKKp zATC>-b8uO9e@`co3>+G$MV`r{%P_IU!-5pu|j#Db;sFt{LR0|e@6D@uTL#4YC^dVMZ%8055Y{x zZW*JkpZoOa7y8UAr{!Q&CVspM1*jfWiB4sPKvC# zHr8Lh#5aS*EIjTB8}~ zD*%6fnhmsC=M7XiW5dHGj#^#xA6}soJQ}U^Fdt=*VP4Y5M@0&b-XT?p>1cucG-Q<- zYEvNxEqvbjxnU)7fS7~$J)Wy+JXqY#7wNfh9a5>~*gkVh7b zhG-bd`^vFN(JPLG+OrlU81k;dR&oP8*o)sr66UY1e7*dIuGxs1B5@#k5w|u4Y2|X4 ze|XaZT~cTSkJM?7@)=}0my^nP%@ADhA2|wK5!YUE#=)~ZW6}u|Z1Ff_F!31f73n6j8B6tgrlhWb z2SN+uz8QD0^N+@zR{aJ)9VLYcs=h~KMtyxF6$0H%#u3G0pY?Bx4H|U9a*VX2Nh9p* zhciTxU*W?1%>mRO3Odyq;&OIB+I4w@w#j}cL}*GXK%u-pf~Hob4E^3y!-yYCDs z>ID{T8oJ~^nQ$8{ROTPx2L)_6R}!5WFCom#&+-xL7TnOtTR7;&CStRPoO;x^>L1`Y zOL{zd6UJCFzsM|PWYbtI@^+ZBhFos*rFaQ1#bY!94Y zRo$4R!<@n;`2y$OYQ#V-)XUzFq0WH||3PvCd#3u1HFD@yHaj5Sa0l^=gOkT(GGyZRiDcWzSF${H>f$N&rqm{|{`3U=R z))nPXPds34au#nREZw@nB}3=}~t0cAjc-J8%g@~NOe%+0}C zNbIZfu`+@mJ|tQmGm{r#khNGs1KlQaLt1CNA4lY zy0f@c(t|+wGegSJ3D%_({{{je^!``Hx2XH`-!sX{rW*Qq-scmCM59~dnmM=oX-cv0 z@!RR;Bj^V)v`I><&Z~e#8{;2WDZf-I_%tCg7p{)QaFBwcjaXeZ;q^42mbZ7-Y|~L6 z>D=Onv8bsR-TukNeYa4-(#XJ(c;+uPxR|eP-+xm0DX|aNpx}Sr>T*#ntyNK4(<58# zrDlHM#sg22%zqTBdL@n!dya?hLqfS)&IG8jUdC0!clO5-7_L2|IDRyjO$M7L%oM1Q zaa?Z^@n6q3c7$tn3ThtOoK7w-d^cO(oB~iTbuaw%ltV-;O56SwZ?|TS`~I<0xV)EK zx~zbl4t^%pop=GR29c3c^^+xtJTTHBdZ+O~+kmO1w?m-!b!3bJU2^Bd=~0&dn@Nq% zq3+`L1RXy$7r`Is;}-S0EX-*V8o`8hI(QMi{)dK@n^sgZ^a$+T3XdTd@r@;vn$#-3 zusz3_;v23aYf>Jy45CD#>L;_?9xEQ%Fw`QKBt)vc)LcUvI2cXOm(ZI#k?p8&ijG>o z0gE(3vUl9{dA+du9#@L}EnzWtCdOv-oeEd{S9nLz+UK}@b$vkx_a`RnTlp&+EZ=CXJ$}*4$?)Ly!C{IkSwwAMt zWbYv%=u=Gbw+dr#=6DV&?O7sbg5zB?TU(I&hg)0!Q7AC`;r;RzDZayfY!U z1>$hRO`|n8?UVu!*pTepjMM&i^w7wod-2EZ~x=19(2nRKgv z^wV>*1&XNTcjRJXw^8d+SqQYSZ0!Rr%Rtaq5ceo3C?$cQMqL8JxKcREk96W%dixWK z?P`Ae*?|PiL-WwZMAfsnd>Rqbujr!kP8d=GCzHZu{4-;$+%7U>U~Ge9qQD1pYB4!N z1knPO(Och5S30aui!$$8citflR5FK5&nW0Qs6aGlQh9$%?l{Pj3P82g>4u zZ05$qN8n?RWaugU+l;)nvPAm80zCpm?(;$;b^&{p!jvIYz(3bJtyt^Y%YC8C(;i^SX|qL<3bCzbmy%#{ z`P)k%1mSL5rNN5L_}yjj&EBJC_9ajyMO%2L4{q2KDVL`lDfIK5VFz)~W8xu6SyDNM z+bB(YLuHN>KXcBad0W+al(O}#N^C6j^pi((?8KBQ5j!Jb%} zyh@CbE_-#>VH7kFIDX#pz(Ga*TK#cm?13epfyaKt5!XPS=C&_0KH+~#`qpHcB9bKs zbp6GzYQ+?BAKgC5St%WYebqi#T%v%3A-*)mYnJcENw}*BP+94anZ-}p>HS3ddOwU4 z<$@&49J|Rp6Q0_+HA2(U7;LZ13CdT6<^9!wgKDBZ%<2^oc}=E}BA?9H_ROfi|I*VV z>1&OJ;tDv9%}JM$c=(R{d%(DCbK}i2{v!|-`)mspiDMosG3o?@q(Z}ZOGrNT8_-dh zwX-^IhOQ7Rdgl&-csiM;%CeBG5E z-xOcZnaLoy2jqcYKUYs?>^-a+i=uFqXbM+KBuZioPU2?ynHt2nI+bBax?F03)V@#n z`HhYBKZ3=F39%mw#J{%e-rPe}@blLe;(CUh;1F;%I2%R2q~bPb7^N>Oay`AS+y z8XL&2)$gz&eUY9%+pxYiDJ`5Z#XpRSh zP1n2GPm`WEqCbd|H_lnpG!ed3ZfPTeq0{s{Vq4}CI62Ff@@1Fk4aHmjHMJ?m?>bPP z&;xx^n{%dV^NgR3_#!TLS=+RquC2_$>ikH|nKt3$#<`q!wON?Cg;>!41ZQTS~y{Nk-0ECVp3^ z1{NGO1iCqdhdr6phrS_U>as#z)4zHOJQOS#NLf+5h`xJc1F4fQMkOb2 z!QEWp_`1(l?!$w~jXFg>D-(T>{L&k{s@N5r4N7%8t;*`*r5}pdmxzVH$l(J0-Uf+b z7I>#H3tyMvss{UA-MocglKvu!>?+Ei$zMwH2epKtEFi-mGUfoT#qW`go!}B}3Vm9< z{}>u#PmAHiq~xXy-av|y(UlMPS8UFv;+|CvBEMY~CJ2&^^mX4^a>VWSQYe&E3n(Of zNKtBr&$pvpSyB?Y7r(0>)-i&+kiT(}p_&F3VJIjpHl;g^X$Pf7sJF;LvBT zVTyeizHI+%hhNfFiezN@3$R+eRzM@D+)oH!E{?*lzimNAa@*X=$D5_bLPAQ~FO98c zy3#zpeaR|kIOU_|WeYBfDmsttJfX!Ftf+9kIkYS9IBTwblS7>+wGEB!A7x1)|AaM( zZ)N{6JZ|?PeJ=4nSj@XN$h}ktd$Ayl>xo(icv!+Xe^FCR+daw_YFEoJrbeI#;ZenQSw@I?xHmcSTiLxg+~3>Wr!`C1 zDVxVp~>ru?H=i@?0od}a<&B0-(P**rGn!N4bg^ihHq%E{|a@PS=Jzd?)->)>ve!S z^nzq2U|o=^F$h%s$Cpq!X%U5{_21ukAD45ixP%0c74kZ78!4|2jk)_kaxouf*JyVe zapF;GRcE#E-Tto#y7Kxn{gNQa2kaOu>6D#jE*@QY*xe?N(}LXfF@sy`5-U?nf*lsK z&S8=XT>q~eP$y)KRP0AEwr;Jtlg;caDm7qvpEQ|*hMI4QFK}Z}nX5C>VIjpP52<~< z;!^?(-N#O0>pPhCc78u2rZoYvO6)Gm&I>{@pn+p->0)6HAd=C!gnt$!jOm7VV5qc< zME@sQ0d6bencVYtrU`A6z_R>1q@&f`cv5 z4dJ-AAjW&GrL-0CMo2DbP^tTPXIjYDdxp&szKcQ=l3Me~LfQO=?b}>BK z@*V~IyT9Dyw3kRQ-XBCU9Q#(dFxR(j#K4%Xx4DO3Y;&(puZ;_-62JbVCfQ$kG$)Ku zB?$Jp9MHovvm@op!DfuZnCom~A0Bql)TCb!ZWqYSX)D=_Cy~T2!L@?_DGhnav!*Y$ zwh{Z<0C+E_miHrJ2j)=7l%2lFZ%!wYDnk4|@$9Sxo_6J}6P_r3S9A+Rc-I8g;%Fg9 z{Qg!n*v{Q};e>2tE2a%wR`mQzSDWQx*v0du8;h*@XvYVx!P6iKh#nn+kmg41@^!Q} zkEom(>A}M&EHy&ib`D+Lc7|^QsUVF3$|b8b2tmhR)6^Uz5VkZ=RUP^w_eL|fpz`kuEg_4CIhTV>cQNHr5ew0n2 zQ}c*)GBmxUm&YaAF0$YUjQW;XX-q(6hA|}$oN7Q{a0Y>w%q_8P3Nj|T^#weYcMlwN z2<_fZgX^85HJ?jKY~Ap3mlgJvIQtcjdIzd~_U7m=#Hb_|u}M-?A`iE}&1lQ9`?yan zGKk!2$5kfN55-sa+!U}Sk$<|sA_d&BkFIiP7)vY+k4G$;7j5( zOU+h^R!s?fBQ2nRj#G^znW7w)Tv{0hZ(-h#uM%^ZhnGXGj~lp? zH&M4cj_O!(xlk$o+b)&-1ZGoFv@7e$#i=k`h=Kti{sshjWnAoMMW1vT24_gQv1{`W zb4b-6=4sn$Yn8t5_<}@Jwsy%iS;O@F)-c=tEq}KUHc2OnGiAmY>TCxlAI}7Z}uM$7*UbRrx@PyL7n*et0ZzNxC7Vd^PU)lZxw_p z5MxwD#EU>|ELY*!@f|-31Y}_XSvUH3--d_H8M8tE8KT!|>&9vaZSm5rwUn2(_RPcj zsI#dnbb>&Ec~gWl;JBsGT_atmxp#6z3(LSRhrlVti1nRkvjdc#>0!+@Bxd9f?3&rh-YN~V8gZ;5sx6H#?1 z_-HTrYjbOIgx5|rO1LR6kp^Ol2_tM(Noaa-*O?TOM=jE3UwQ@;*8ANmi9L@2F(TVf zI7ygO*523;mrrC&zwLdwA$clXDl@s-C{nW*dda0oZ8OK=d~z?W%jk4Cr-C&uQxgqsRU#`8`Ju{lO4 zl(lBaXmmFtVaH>%LCFP;R8TeLwKsL|SsYGYP0B3-Ry()pPx-HAbg@t@P{5m%lIqic zIKs8nSQt;|MihQ%5c(WL zjk#hSQV~$1HCT1?PKkcaeV383SqLsARh0&Hv$e@|1obxc-bl)^8HB0wUt;0xdkp1F z&c&!{H;uY^^H)8^(t|E1^Z5y^22?>4^>d54J4r^3iv`?j?hW#8t9tBXD5FR`zio_T zHUS}le~SimTV#eg!p{9&HqjL7=u(BSm=zQ}2I>M#Rq04bD0{&LZ;eaHgzJplllNg$ ziCk&S4%5WuST_2pID5j7T8fi1r+POzY9j7UO=NYn(Ca>jqQr=1L{p+@vFz_->@Q}K(8^_nhpnnZ1%tgApb?UTG?6!i0RT4rN z&5=MqtY{gDisc$|5nuCTf;;d>`j0&XpWWn6eP~#lSlb}Zzt^7k9dBJ|tnMH;-c*i&*f zzttoeyTxw$i*H7*lW231^`P!=Q_wNrO7{5a20YTy^}Vs2rOo%=Cs>!pSPU|`-klEI zz0$qQ;HeP7`n`0!&u8f(COKG(=t<|P?@gJKN;JnH46n7DTSHv(%M$%N2myQ*!30B> zT*SSl!OE;m?Kc{9-SXZ3OPe2f4A@M5^$*iN%A{s4_ql#y_-~rPuaoAm z9_t!>XX)naUO~gw;J7HhX3FDoqxfZ9(rvzo=o^~J-7Tqv^~X|+rF1o1Gw0v=%p;uN zBNnLZDH58o{@>DF*qj6GzH6{p~ zFxq0_&|zg1G(L7bmGU4X?q~zOe5Di?^^g%(iYk$?t>kf8 zv&zru+z?zoPv!E%+J;Ap+rRr8*=&otN+?=>j<1+S->ukxTjjGN53ZBGBB;}KfRd-2 zPx`G;xv`Qn$Hre%%}7XeB6PFt`UVc?x)`Iy<}?w0--3pLk`j`NpxzYbh;*M{<__#p z-=y9|gt&>as7TM;5Ue*Jobu4#fZAC6oa{7|Qt67wFVDUw;5o)~{3g^Hdw~3So0*w! zR0HHfijbU*T3ryMK7B|w{G3$QvTDghFQS^08d*a?-^TnI;lK~HkUTL!uW_E3%7lJ} zSIp~GiWDL|M7{c33Kja7cAGfwq^Q9EYZ4gY`1zu*&wRRsmlQSBv9e2lzqzQV2l$#i z^TXUAxuBkuAIw269sozgY4jVEk$RdXVEW5Gd$s0`mjqIECl{+#? z-cb1HJo6k!w(zHhB}9`vLUy?}+2mZz?5^|B)~jTlnX}}#xfg(ZW4oe`=swDuk2ZcA z(TIZ9*HLU(N}I6r{jK`_VG6TIX#`rU*tt%>ext3h_Re#V4Kq_ zMXqs->K1MKj_+;21RkKFATP19tn-NyO0@dse99vZa~CoHw?8JL8LO4!LmbLTk%qE6 zw8Ul(=c#!ed4$8+k>Aij3;zGKFyDr+TNkD31&u1|n=tVe54N}{+~{IHoVg>M<(*UW zg}cZ%;Z9d!!+I{JsORep)_BX15RkjbgI(>Jd|pJQNQTX~QXc zPoc(_8QU+;TD0ZzFH&{q=-S0CT>qu)EeG&9DwlS1OC_nuGB_w;{9?^%r?l0~AuF8i zSEZ4#(vNVaKTbOEf*Z>~WLxr&BNQFbQI%6@^>v=gX4BGk!{;O@)jAYzXuO{?c~|BH z?XPLF(T}TY@TM=NK+Pu`2-SfFo@?8!AN$V9`s@{5B>8K&zp+h{KAg8JlKF)} z*o#xq05fHVTlNvIn-V#X5SLtda46{%w)1oHg{=m^N&LNdTeglp_^(CEsw2%wlU2F5 z+NybZR3ilPe_Vj-d(v8i2DGM7(tu-Rus}H_l+yR&ML4;zGNWbb=099JJNm}i>B79v+7KWFJ;*4#n$EdEqs^q5o z3Z#_6^IG9Ks=*#|zV9oI)ok@91yMPwWFy$%FnafLZGGUA?I;7Z^sLr{{6M#h!&w*}vXMS#&|BN!SQ6qZYPgylu00DzgXa6)8R2aG0hltoB zs2&Gsvd3XwoSD3UThn*)-5q7JQ@N^KJdvm+EAh30B&Yc>be~;bFV5$BaHP#;@$D*J zXJw^6FBs3q%qr7Gv^2yB#0rI2;-R9+O@-JZEMGpVEH~G2dYWi8TR3s@O#3_Ge6%1e zXo2V?iM>N|Ci(di*rZjA7sn4#3Cn}?m>ygp$hYLZr839=&dX1gM-IX>AUboI9SPkbIbEog)V@nPKf}O_6L+0amIpU_9GoKr zv#wx%Mo#QfQW=*^>s=_8ln`K_+;oU7!sxOQO&!I91Ps6_VCKE$ZIm6dttz=C zy*543wVI2;fdLI=s>0JWqxZRdWeSZ(i<7PgBeIiRRFWqoIYqkaI?0*j*Cj0@>SN{; zjG!I`UT#KTQhQ3!7g;5e?jXY)!x}&YB&P(@`Qu(7HxsWd!tn60k`4lZa5uutPeN(L z&rz_cuLzwg*oM4xo7vf)11V>%~1C&}?cpuBid$*ar zxAUOd|mq~Cy6!Y5g-Uug2*JU;;Cy`|=q9Af>|CN(th6a-pd`032$*D{cw{G1* z%VX>&tp$Cu)5~D!%?T39B{_LH-FBz##px(ec+p7CyA=`KiL>BrektmQ)AiokUrGMZ zGH?0pUj|YNBe$?|Sr9oeI1ra8R$M%{0RZmaolWZGeUzbA$eieS-#9V^3FVQTZaKRT zU&mil)hytmOTc7e#EP}AE3+St$(nh*xAq0)$B+_|_d$^MKe@0Wdo=XJ&#n z;mL_kuc!tcSS30Aho#d#b)~E3x9P=EDqO*k;VuTj`TTx3t^N{4jyDQZOLjGQ78FTt z7UadnxQ@e5OhfT_M-h3cU}3RtN0xGCekL<-PLQmWs84d02~|xzMGIb>c*>d*!JT;p zz%;A4-%1|Ume+d&pg!FyC8wL-TQTHD@);GA#S%drN0~8v4#Am_m5<7KEIoedrH z@<<`%mS3Ex4e{r;F+e*9*4ZSqG(5u*R3)2~iB2~qotRXVhvbCFcx8G|M8_Gi;kZ~W z6N7-@{t}D>o_VJV|0RGAWUkV1Q$*LXcoT@)!NrC*h7bbBb=wt!f@+4^X?ey)v$A6U z&-36_+D)t3!rc6VvP_3KaAfDP<^ZJ>tybPCBFWz_+!I2wLoaWWOv!$i%nN$uItkM4 zFH>cOdbs(erNlxQ$=S^@*=4k^rjn@di+JKMIp(qO;6|W%iIrX(sG&9_7?I1qY83KH zNGIko>F|K#I3SM8vMSivk?QR|W-~M`l-x2G%@ovX1BQfd`h)E4V8Y+Y+N`9yJjzc} z6sTjO(ylL*^{1p9HO(t7Lz&5fys@NKvxQcJ*{$IN-X`GewR8gK6@BaqI2&Mko>zbi ztCCTp(GUXHt+h{tXSH|7b>O(Jy2yl}?voog0<%PJ0O0#F4=WwS<_%`;$rIimgE&$> zy38A{>P6Otg{nHGm`;r5edOhh74Z2U|0^tV1GoeS0Qd{mMe0-0Fs;HBPD!4tK$w3D z<+!e!y*n&y_@t?{oVX0z zyn+NbGyaYjqpLrur&$gF{YlozaN~LQ;v}0Egu7=kv zkXu}Xi5R)Zk>6{RN}!!kH7}9rLa0xgpe9?bC_BL#$q59hdw)yQixY>Z{{Jh|E+M!r zUj=ZTm6Gg-Q+)7@6cW1>%~|U(3+FbZMoFcbHJ%Uw0U;Xm{G{^2{~MB<_+r7t7RyO} zD%}&X4Cry1I?<_JWG9qCazZ4W0UeJ_QHN%ov+czR!qX|>{>%dVI3xu3dk(#|=}xrUzTzTROpMlUXhJIBvMq zN_1QDd2j6^5)V>IMvn4DlACf~lL1qELT?I~YcRq@sve?iA@C8-^r8F5IYy2wp5GjX5 zotIJ!ftQlE(OC*jEs>izRZeUXvgc9SA}qVaCnq|we0ArqOp;Ug;>@2x5qbIfMN)j9 z3Pw-qpMbHrJ`Kgy)b#gxDPC&J8;vTXe2<7?Uj0YP@ZJFJHL6*O)gU(|uT7IjI?k;iE_Cp4Gn_xKMR0)N=U2O;!Si9QI-p@ zrjD1KXBvcQXj&e*<@4I&SluWWAn%&C-Ns z8(hLuDy1f${e^i!Bb)i%=$aR&DwCN>_xH;42ZNVfKKUGQ`mU-r-7Q;xmzU6L^5)x- z>^H@GVpLTOytfXy`~{7rqP=lGE|xEK^ZoqHNcJ zxI8|W002=APXTR$JN+VnH&`jY=Sxn!DFQj&dy>y{>qxUeNI7bZEpn6d27%+KZxUJ* z+I9*Jw&Bf{Npce8_TqF^bty~*g~xZ64g;rN(vfDe>Hcr>5}HEhRfXPD$@3VJ&i2zv8U^R^V_`Cq=ELBot4-02K&?xmK-HIV9Eji zCEOiy3;qZf7iz7jF+#GTl8rc={D}<`jQlbQP&&>K@ zgN85_c@hWdH-U4D2C7Z4Z!J(i4`5mHIt3vF!^=j(P=@hv6*~>&p^)B=z42oMy;dt_ zi~eme_1<*71gVVpRfW1v!VL?ePgKrch0ZI<&Lm>?=}qc7v`>H1xF9!)3bgU%tK^rS z!j(CJOPrr7+@B~(cJkKZe4W>iKL^yd0mp9&jbXs>wR~h6z#nF>6Hp4Y8cjwDly%6>=JjV0y7z?vujRwzPci!faC*{6Fme$b z0N~dFq$~?Oq*kNZ4$w`!twNDQNwvJUq#ds7#)yqm%81-jqz0b1RREZtoj%ctZ#iF< zPWzQxq!6mwQ^w6-oiH^h*<^HSh42CIUmA-?Zi1V91DU8WX3l9TA#xi#(A2}*N2G`YOEiifETcr8z~K;U!0 z*~59{Td;q}ZooMTAT!0C5MX54NZ?TjH6=qHF^Ej>#TA>OKcXu96;vr#Nu#M0i*t*- zBS<~Kc^Ei(Tx_Tq7A7~HISJr%tQ1Ox?q>!?w;^IHt}+#o(|aS119xCRvL4hjxpmQF zlOg#y^}RWxelmq@7P_T$RT{|aewf;7dV=$n>2rC>4X{FR06_m40OqJdv(eJn$6p51 zX4?6}K5DQSXpxXYF8MJ1IUK0I)$YdBq<8|bcrP-xf4q4HZgW>=H4sqb6$3>JS8FgauOwkvi%Qz`> zAKbfBV24gOfgMl^96xS6K$ZX@h=o7l7g-r@E`=0{3N;H<@>*1+lV4Y^CR&Y_VATTo zOfX9$KP(X(0C=AO+@Aq3oJnbU@LE3Hn&hBnyIwQ5jN`g?cJ39c(N59NB`Cq*;+;FU z!g0mVpiHODrxeGJ$Ln$aG|H<Eu10G6Il7Jm$F;wKqy738kSn{-!qj`SPi`4XQem{%-*U3kw5LGw zQI#n6;`H4*E5D=w;!*+)IDilGtH5FAxi6+DAWoZn4#0Ou&ga2&Lc(andC%VUU*s?%_OQv0)*%dJSfXGk!IHff!v}LsoId# zPsIZZKH&T^3NW~!p7zrR0dRNUtS=@99PGD`e4b%62;qbwtQ-G0Zm=(_ ztOdr32`MftP1tPEM!@~v(6 zWLB=gZY{a;j9QJ*U&CHD$qDl6nW84IXc%=OnxWP7M8^Q$1pHskuZ4{Q3GUog035?9 zn)ODm)2!^kq@yUmq;y^az;$a%qq@wHR@r+qN2y+$$cT(LXF_r&Kfq%Dnz5*2`r(S2 zR00HD1C2$|6fyeufywJd=`VOP*y;ZW(63~#2Rr}P=j&!-yV_q@6iBe!ZhTbNsy*hR zY1h0q9x~pX2FWRwF?wMc->9P8zwSS<`a)gQ&;0dpP_i%-J=>K6wO;_h82Y8~JU{4o ztT16&F-=2Abm$8&rV6%1ilr%eZ|1+Ea^x1ll;m=Cr2AlA7-wLoq(^aYcG`rnjR+?PZfrgv{VrVv+qB9m20slh*ClrqoSDc@G4WR!k zFSSu`isJR+(R=l_FcZv7<`A1s(HQ9v1EMn-bWoRBE{yx(j6GUAztr=5$*3v$N5Gla ztteAK$y;hn{%Zh#!Ao5fkGKRVvBs%YAoUC?{ck!@9$YX{p;g9^*ra+jNY0>Po69b| zU{8WlipBZp@OfrC{%^pk-zw3hvI!0pa6by*1~bv8STvnFl152&tBC%$auqYgnvh#w zVoQrmcyrp;XlAzKDHALFHZEe{R; zA+SO$vHFsNStHb*!Ed0W`0!qlPd&q3FPEc%ukcyc;@ampR=rBtontiP?M)G7HiaQaWnvL}u%ZfiUG_W&H> zr6%27dob@nuNJy%QAy;MQm3TQINGej%!I@J4J_P3R8bNC#tq{CX`)Ck&IrX% zzc8NhVbuyA0tX~mg~U7EntmiWaj$!*z{xA8{}jOg!Aos6TG9h+a-;Gu`N|u}MMP}b z-w>xfeVJ8-_DHHNeHIi5i=`6liw)TsEB^wR{Gyp9_D}(W1Ax=N2EqKe`eFmkTnE9 zRR|8ik!gVZO#rjZWC|?IEs74U=pzp-HeU0|%2nxKyH_E;xt{06Rw^(t19(3MoMcYq z?*=MFZ~$=X5I}zt05cw?lwx6Sv4HbI&Csp3*3~v{d&VsheleL z2%J4!o>g*GjNkxZ@-G1VA}`hVd@Rf^h;DQree_`w)TdaOTaY;xzXY8Aa#=R1qJjhm z0H=QsAlwtKJu?&+<~emWKs#e?T97^pOKWm(YS}t{VV+9yrb22CdtuFu{QWiysB>A}>R$*^+g7 z_R&Wl2B4H;VRj+5aV67ZegSBG2tZ{={;EuH04^>7tsepKDlbQ~(F&Jz^$|z6qIV;` zl>5cDralxvDPrWtz0a=$Lq7(btF(tvae@PYbGHG!55O^Aj%K|XtcmMa`r@H(Aq^>mAs_`Z+%nFY~FSU_;hzoiA{H0wOQo(#5RPMN{8;1rQc9Pm$yl!) z!>Tv4(x4yIr01DwBAhFxV~2@xCsoUQ|t9|Z6T zQ7*6LVfOAES}k$ET&Z*6bmh`wOfelJP5)`j2#rZ3siN{yRGX1>k(FC^^w6j`!r25e z&Gff`(?1!k|6V#+Vgv^Opup*W2H>Lrxc)e$fy-WBthY;9Q_uNob)Z4R7Na@Od4^z3 zh>jvvPLtY-B>XP)X{d~#ieA7J6bthUDqD(q0RBf{^5dxRt{|pZ;=2a!dJjPVGk}%s zbP9lAV6cXfWg{Sr*e0t>_js0H{neh&L-W!|XR6|sW2Pcp#Rw=kgG+dc5w-5_SgL=-uEI@u5IQ8eO z#HD~GPH+IQ>oLIpB0#o_aygEJk!8cE4b&3X3F})?1jkn)l&wQ@axf^}pq8X)QUQij zKn%zY$U|^C{ck!1NBQ|Ud0Cb;8%@=vie~}uzpL6A3bx^P+koOReHGaB9N;|$V1k{8 zg3_RJfKt?IH4f46*@u~r@l{G(T_?Dt4jV_2ii+ln{tRcT6kKtBvjeo-a!Qb84}YZtI% z7vOvWz+4%AZW6_PtmA1Kq*igBYq5E000Pm zNklp7C+PPb7H=!zh>auE!~)EDZQpIcCv+Xeu54jBD$;23wK z*rkkn)Psv-=MMqoGXOToayqVq(PbmSAOz6mFqJL8m{2vFxm2R#4kl=jn__6%&SBAe z%Usn7Ph%HbtyXYGsd!_`2vYz)3Y`8^mCQ>OeS!l3Ja8XS`+b1^2tQA!<92Xp03)L# zAdYbCG<&7sFtu1OE~}HUFWZ9!uMI$IqO)D^t&H&0r#=?v7h~&4xq}E%`d5J3-vLhC zlU`dKeS+(N06RYhklz3>BFpSJ1S88vFfcff7Lpaq`Pw}%u2U{A$uTU*SF53Sd#nRvcD{sBt2o6(HTaL2$|F?H8F>)Mb0RF10A7ewXq7XR5IN9}r6@rq8 zaDX_69Jm4kVL%9kaHL$xB}x>@2qA8fqDV*}f@CB|IaxVD1P2Qd6!{W-fXIfJUB^md zB0y_{jJ-3{-Blc_cOJVvJ=L$7o!R=N(d=|rRkvnqKK=FRD&R-3{ya>z#9ow+zs+%Ru7$>m%-_sE2k1;5uO#0E{stvFxl=xxe@(NjsB zuywmzX0g^kfjS9S-m=ZGAK0gZ3`K1I6COMbH?ISa0paAddN2r)rV8Jec;3#RcyUMF ze51Hw-s+OexxUqzAr_6s(#&3NHa9iBe3gxkc5ioW2NAY_Q?Py#ZY;!BezU|yaD^OO zf%F%c?Fy}kIw75d9lSAPv>nCeUt_kCn{XeMs5fHSjNFRzHa)nYl%~7YrPuB6IA8dE zz7Fb1vk{Xc4<|S))=mIF13qe#S_nbsKtyyP8XdD7Nl^H4_t7d*Tz06m0c<~f_Mj@Sp z5uWhMMR6m4w%b(QUB+G(U6rV9kg|5aTYmc7xok>hV(ZyX`Pk+f=;vVL<-(hDsB zmAQTiIxj(R6ZkxZqpd?jq)AG@Ybv>va%{42YfaVcPsg_czOAbPU-;Rd$hCJp$e-;CnzYE%hj27!Y+L{LpVOikpz4rsM{n z=2&a$h;5L%q};~N+C*;};y9+)?Gg8rkuz3bTnyq@pnd`y@7ZK239^?AoS)SF`5mLT|Rb zmvDk>g<~tAo(25`6}kb!$V7EPu+x!co2nC1SPRm!Jlc_6epgk+iN6!GmY8OoV>+Eh zYyDo|X3x<)GO@{1@b=qP3UExu39eO+eg?!3fv-_Ybf!5YrAHWrgi)BSN0)E5*-LKv zl}k5eca4PRB&a~~U}tr?rAca{wO-%$iG!V_z^lNIVf|v20vywEf}07)Rv>u_#1p`W z?DCBF*+?k~qac%A=E}N~%MLeF37W1-^#=?mM{U!~&y$-{DgB=5TTAVhEXv~+h*!XS z!9{HC;{-QT);RrLF&`B`!l zbSM7;eh&Um;cuID3Ub)v1UDDfBH%l~53pD^y$I<`f*>FaeY4`QRcBb+(cJu)GXJ*d z#Nce2q{MyGua?-a<}r!4fER&RVBKo-1V?i$dVO93jy?k3lc2v1e7H_YLV)l41YtlB z2Katax8JOzxpt`|Hl-ACoDlaD;(lCZ1av1|;Md^21eed%EzD7!MXSe4%&|Kl`6}>T z5MQib&am&!^E`qqvh#gEM|cJQ{sj@;bg}I|skI&xTAJ35(zdw(;$`Tafp>2-Rj8vn zd%gbdQ$G1Ha1!(hEcS}9MF^0Y=zQPD4`eoAv3R1h6`R(OCJ9NBk|YU9oE8YpdNS^4 zo)3VtAWy^PKbOqxXqmlMZyn=!u=*&76Cl1pE!l1Vc*lRlo+psLkCYPWd*)pZH@zNX z&a#`f{E$i&X{t!mOlC=1W<}gGTnAnQ&cfl}!XIjlJ~-OL32t#1mEE_1kJT%#I9d_H zEb}C#k)9_#Ja79Y5Sakmu0W$6(@LR~MkymPm6&()YJCfW8^CXXvvBBlaG}zyUdL>4 zf?G@^9DO+JtNRl01t2Hns>*1z2a~GFKzLH%3D1n+3bK)0F?i%`^tjf;8NH*cYG|zo zpH(I@trSY@f!OB2CCp&KIk@9>C$+`H32rG^yBm1i2v2{R2K(*osnUN{r|ed)w+Z|n z^f{2P!R2dBU%)Y2oZyxZ32P65J`UnIW?|05^S-G)%KISR0CnEHum1(}2$f?=aDv+x ztR4aR80bea!uvFZ>(?zMec&Q+9>jS_-+(Lc&ikeu?cxNtFL~UDe;fhz0PqM#cn<*g zvlMkge}IwMCE$YjJG}^3yYsy-$E;a`%Is*4BX>e@KZsQj_kmsk?#o`EpyXnnEfNFo z0at-{KwJfV74#K||LP{$I<|3wbJ!wb3-gH6EfAZaH-UefcQ5;_H$h#4cizYKl{qZ + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/kosmorrolib/data.py b/kosmorrolib/data.py index 6a66157..3d3de80 100644 --- a/kosmorrolib/data.py +++ b/kosmorrolib/data.py @@ -36,7 +36,8 @@ MOON_PHASES = { 'FULL_MOON': _('Full Moon'), 'WANING_GIBBOUS': _('Waning gibbous'), 'LAST_QUARTER': _('Last Quarter'), - 'WANING_CRESCENT': _('Waning crescent') + 'WANING_CRESCENT': _('Waning crescent'), + 'UNKNOWN': _('Unavailable') } EVENTS = { @@ -54,7 +55,7 @@ class Serializable(ABC): class MoonPhase(Serializable): - def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[datetime, None]): + def __init__(self, identifier: str, time: datetime = None, next_phase_date: datetime = None): if identifier not in MOON_PHASES.keys(): raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()), identifier)) diff --git a/kosmorrolib/dumper.py b/kosmorrolib/dumper.py index a21e9b1..53d8701 100644 --- a/kosmorrolib/dumper.py +++ b/kosmorrolib/dumper.py @@ -25,7 +25,7 @@ from tabulate import tabulate from numpy import int64 from termcolor import colored from .data import ASTERS, Object, AsterEphemerides, MoonPhase, Event -from .i18n import _ +from .i18n import _, FULL_DATE_FORMAT, SHORT_DATETIME_FORMAT, TIME_FORMAT from .version import VERSION from .exceptions import UnavailableFeatureError try: @@ -33,12 +33,6 @@ try: except ImportError: build_pdf = None -FULL_DATE_FORMAT = _('{day_of_week} {month} {day_number}, {year}').format(day_of_week='%A', month='%B', - day_number='%d', year='%Y') -SHORT_DATETIME_FORMAT = _('{month} {day_number}, {hours}:{minutes}').format(month='%b', day_number='%d', - hours='%H', minutes='%M') -TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M') - class Dumper(ABC): def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None, @@ -60,6 +54,9 @@ class Dumper(ABC): return date + def __str__(self): + return self.to_string() + @abstractmethod def to_string(self): pass @@ -189,6 +186,9 @@ class TextDumper(Dumper): return tabulate(data, tablefmt='plain', stralign='left') def get_moon(self, moon_phase: MoonPhase) -> str: + if moon_phase is None: + return _('Moon phase is unavailable for this date.') + current_moon_phase = ' '.join([self.style(_('Moon phase:'), 'strong'), moon_phase.get_phase()]) new_moon_phase = _('{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}').format( next_moon_phase=moon_phase.get_next_phase_name(), @@ -212,6 +212,10 @@ class _LatexDumper(Dumper): def _make_document(self, template: str) -> str: kosmorro_logo_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'assets', 'png', 'kosmorro-logo.png') + + if self.moon_phase is None: + self.moon_phase = MoonPhase('UNKNOWN') + moon_phase_graphics = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'assets', 'moonphases', 'png', '.'.join([self.moon_phase.identifier.lower().replace('_', '-'), diff --git a/kosmorrolib/ephemerides.py b/kosmorrolib/ephemerides.py index ec50b88..33642a7 100644 --- a/kosmorrolib/ephemerides.py +++ b/kosmorrolib/ephemerides.py @@ -21,15 +21,17 @@ import datetime from skyfield.searchlib import find_discrete, find_maxima from skyfield.timelib import Time from skyfield.constants import tau +from skyfield.errors import EphemerisRangeError from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS, skyfield_to_moon_phase from .dateutil import translate_to_timezone from .core import get_skf_objects, get_timescale, get_iau2000b +from .exceptions import OutOfRangeDateError RISEN_ANGLE = -0.8333 -def get_moon_phase(compute_date: datetime.date) -> MoonPhase: +def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase: earth = get_skf_objects()['earth'] moon = get_skf_objects()['moon'] sun = get_skf_objects()['sun'] @@ -47,7 +49,16 @@ def get_moon_phase(compute_date: datetime.date) -> MoonPhase: time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10) time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10) - times, phase = find_discrete(time1, time2, moon_phase_at) + try: + times, phase = find_discrete(time1, time2, moon_phase_at) + except EphemerisRangeError as error: + start = translate_to_timezone(error.start_time.utc_datetime(), timezone) + end = translate_to_timezone(error.end_time.utc_datetime(), timezone) + + start = datetime.date(start.year, start.month, start.day) + datetime.timedelta(days=12) + end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12) + + raise OutOfRangeDateError(start, end) return skyfield_to_moon_phase(times, phase, today) @@ -71,34 +82,43 @@ def get_ephemerides(date: datetime.date, position: Position, timezone: int = 0) start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) end_time = get_timescale().utc(date.year, date.month, date.day, 23 - timezone, 59, 59) - for aster in ASTERS: - rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) - try: - culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12) - culmination_time = culmination_time[0] if len(culmination_time) > 0 else None - except ValueError: - culmination_time = None - - if len(rise_times) == 2: - rise_time = rise_times[0 if arr[0] else 1] - set_time = rise_times[1 if not arr[1] else 0] - else: - rise_time = rise_times[0] if arr[0] else None - set_time = rise_times[0] if not arr[0] else None - - # Convert the Time instances to Python datetime objects - if rise_time is not None: - rise_time = translate_to_timezone(rise_time.utc_datetime().replace(microsecond=0), - to_tz=timezone) - - if culmination_time is not None: - culmination_time = translate_to_timezone(culmination_time.utc_datetime().replace(microsecond=0), - to_tz=timezone) - - if set_time is not None: - set_time = translate_to_timezone(set_time.utc_datetime().replace(microsecond=0), - to_tz=timezone) - - ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster)) + try: + for aster in ASTERS: + rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) + try: + culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12) + culmination_time = culmination_time[0] if len(culmination_time) > 0 else None + except ValueError: + culmination_time = None + + if len(rise_times) == 2: + rise_time = rise_times[0 if arr[0] else 1] + set_time = rise_times[1 if not arr[1] else 0] + else: + rise_time = rise_times[0] if arr[0] else None + set_time = rise_times[0] if not arr[0] else None + + # Convert the Time instances to Python datetime objects + if rise_time is not None: + rise_time = translate_to_timezone(rise_time.utc_datetime().replace(microsecond=0), + to_tz=timezone) + + if culmination_time is not None: + culmination_time = translate_to_timezone(culmination_time.utc_datetime().replace(microsecond=0), + to_tz=timezone) + + if set_time is not None: + set_time = translate_to_timezone(set_time.utc_datetime().replace(microsecond=0), + to_tz=timezone) + + ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster)) + except EphemerisRangeError as error: + start = translate_to_timezone(error.start_time.utc_datetime(), timezone) + end = translate_to_timezone(error.end_time.utc_datetime(), timezone) + + start = datetime.date(start.year, start.month, start.day + 1) + end = datetime.date(end.year, end.month, end.day - 1) + + raise OutOfRangeDateError(start, end) return ephemerides diff --git a/kosmorrolib/events.py b/kosmorrolib/events.py index d7f49a2..cfbf87a 100644 --- a/kosmorrolib/events.py +++ b/kosmorrolib/events.py @@ -18,12 +18,14 @@ from datetime import date as date_type +from skyfield.errors import EphemerisRangeError from skyfield.timelib import Time from skyfield.searchlib import find_discrete, find_maxima from numpy import pi from .data import Event, Star, Planet, ASTERS from .dateutil import translate_to_timezone +from .exceptions import OutOfRangeDateError from .core import get_timescale, get_skf_objects, flatten_list @@ -137,8 +139,17 @@ def search_events(date: date_type, timezone: int = 0) -> [Event]: start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) end_time = get_timescale().utc(date.year, date.month, date.day + 1, -timezone) - return sorted(flatten_list([ - _search_oppositions(start_time, end_time, timezone), - _search_conjunction(start_time, end_time, timezone), - _search_maximal_elongations(start_time, end_time, timezone) - ]), key=lambda event: event.start_time) + try: + return sorted(flatten_list([ + _search_oppositions(start_time, end_time, timezone), + _search_conjunction(start_time, end_time, timezone), + _search_maximal_elongations(start_time, end_time, timezone) + ]), key=lambda event: event.start_time) + except EphemerisRangeError as error: + start_date = translate_to_timezone(error.start_time.utc_datetime(), timezone) + end_date = translate_to_timezone(error.end_time.utc_datetime(), timezone) + + start_date = date_type(start_date.year, start_date.month, start_date.day) + end_date = date_type(end_date.year, end_date.month, end_date.day) + + raise OutOfRangeDateError(start_date, end_date) diff --git a/kosmorrolib/exceptions.py b/kosmorrolib/exceptions.py index d6a87d8..9467d38 100644 --- a/kosmorrolib/exceptions.py +++ b/kosmorrolib/exceptions.py @@ -16,8 +16,21 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from datetime import date +from .i18n import _, SHORT_DATE_FORMAT + class UnavailableFeatureError(RuntimeError): def __init__(self, msg: str): super(UnavailableFeatureError, self).__init__() self.msg = msg + + +class OutOfRangeDateError(RuntimeError): + def __init__(self, min_date: date, max_date: date): + super(OutOfRangeDateError, self).__init__() + self.min_date = min_date + self.max_date = max_date + self.msg = _('The date must be between {minimum_date}' + ' and {maximum_date}').format(minimum_date=min_date.strftime(SHORT_DATE_FORMAT), + maximum_date=max_date.strftime(SHORT_DATE_FORMAT)) diff --git a/kosmorrolib/i18n.py b/kosmorrolib/i18n.py index b5d09b4..d0257b1 100644 --- a/kosmorrolib/i18n.py +++ b/kosmorrolib/i18n.py @@ -24,6 +24,13 @@ _TRANSLATION = gettext.translation('messages', localedir=_LOCALE_DIR, fallback=T _ = _TRANSLATION.gettext +FULL_DATE_FORMAT = _('{day_of_week} {month} {day_number}, {year}').format(day_of_week='%A', month='%B', + day_number='%d', year='%Y') +SHORT_DATETIME_FORMAT = _('{month} {day_number}, {hours}:{minutes}').format(month='%b', day_number='%d', + hours='%H', minutes='%M') +SHORT_DATE_FORMAT = _('{month} {day_number}, {year}').format(month='%b', day_number='%d', year='%Y') +TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M') + def ngettext(msgid1, msgid2, number): # Not using ngettext = _TRANSLATION.ngettext because the linter will give an invalid-name error otherwise diff --git a/kosmorrolib/locales/messages.pot b/kosmorrolib/locales/messages.pot index 71f240c..6d609db 100644 --- a/kosmorrolib/locales/messages.pot +++ b/kosmorrolib/locales/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: kosmorro 0.8.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-06-02 20:49+0200\n" +"POT-Creation-Date: 2020-06-06 16:43+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -59,107 +59,103 @@ msgstr "" msgid "Waning crescent" msgstr "" -#: kosmorrolib/data.py:43 +#: kosmorrolib/data.py:40 +msgid "Unavailable" +msgstr "" + +#: kosmorrolib/data.py:44 #, python-format msgid "%s is in opposition" msgstr "" -#: kosmorrolib/data.py:44 +#: kosmorrolib/data.py:45 #, python-format msgid "%s and %s are in conjunction" msgstr "" -#: kosmorrolib/data.py:45 +#: kosmorrolib/data.py:46 #, python-format msgid "%s occults %s" msgstr "" -#: kosmorrolib/data.py:46 +#: kosmorrolib/data.py:47 #, python-format msgid "%s's largest elongation" msgstr "" -#: kosmorrolib/data.py:261 +#: kosmorrolib/data.py:262 msgid "Sun" msgstr "" -#: kosmorrolib/data.py:262 +#: kosmorrolib/data.py:263 msgid "Moon" msgstr "" -#: kosmorrolib/data.py:263 +#: kosmorrolib/data.py:264 msgid "Mercury" msgstr "" -#: kosmorrolib/data.py:264 +#: kosmorrolib/data.py:265 msgid "Venus" msgstr "" -#: kosmorrolib/data.py:265 +#: kosmorrolib/data.py:266 msgid "Mars" msgstr "" -#: kosmorrolib/data.py:266 +#: kosmorrolib/data.py:267 msgid "Jupiter" msgstr "" -#: kosmorrolib/data.py:267 +#: kosmorrolib/data.py:268 msgid "Saturn" msgstr "" -#: kosmorrolib/data.py:268 +#: kosmorrolib/data.py:269 msgid "Uranus" msgstr "" -#: kosmorrolib/data.py:269 +#: kosmorrolib/data.py:270 msgid "Neptune" msgstr "" -#: kosmorrolib/data.py:270 +#: kosmorrolib/data.py:271 msgid "Pluto" msgstr "" -#: kosmorrolib/dumper.py:36 -msgid "{day_of_week} {month} {day_number}, {year}" -msgstr "" - -#: kosmorrolib/dumper.py:38 -msgid "{month} {day_number}, {hours}:{minutes}" -msgstr "" - -#: kosmorrolib/dumper.py:40 -msgid "{hours}:{minutes}" -msgstr "" - -#: kosmorrolib/dumper.py:120 +#: kosmorrolib/dumper.py:117 msgid "Expected events:" msgstr "" -#: kosmorrolib/dumper.py:124 +#: kosmorrolib/dumper.py:121 msgid "Note: All the hours are given in UTC." msgstr "" -#: kosmorrolib/dumper.py:129 +#: kosmorrolib/dumper.py:126 msgid "Note: All the hours are given in the UTC{offset} timezone." msgstr "" -#: kosmorrolib/dumper.py:175 kosmorrolib/dumper.py:255 +#: kosmorrolib/dumper.py:172 kosmorrolib/dumper.py:259 msgid "Object" msgstr "" -#: kosmorrolib/dumper.py:176 kosmorrolib/dumper.py:256 +#: kosmorrolib/dumper.py:173 kosmorrolib/dumper.py:260 msgid "Rise time" msgstr "" -#: kosmorrolib/dumper.py:177 kosmorrolib/dumper.py:257 +#: kosmorrolib/dumper.py:174 kosmorrolib/dumper.py:261 msgid "Culmination time" msgstr "" -#: kosmorrolib/dumper.py:178 kosmorrolib/dumper.py:258 +#: kosmorrolib/dumper.py:175 kosmorrolib/dumper.py:262 msgid "Set time" msgstr "" -#: kosmorrolib/dumper.py:192 kosmorrolib/dumper.py:262 +#: kosmorrolib/dumper.py:190 +msgid "Moon phase is unavailable for this date." +msgstr "" + +#: kosmorrolib/dumper.py:192 kosmorrolib/dumper.py:266 msgid "Moon phase:" msgstr "" @@ -167,36 +163,36 @@ msgstr "" msgid "{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}" msgstr "" -#: kosmorrolib/dumper.py:242 +#: kosmorrolib/dumper.py:246 msgid "A Summary of your Sky" msgstr "" -#: kosmorrolib/dumper.py:246 +#: kosmorrolib/dumper.py:250 msgid "" "This document summarizes the ephemerides and the events of {date}. It " "aims to help you to prepare your observation session. All the hours are " "given in {timezone}." msgstr "" -#: kosmorrolib/dumper.py:252 +#: kosmorrolib/dumper.py:256 msgid "" "Don't forget to check the weather forecast before you go out with your " "equipment." msgstr "" -#: kosmorrolib/dumper.py:254 +#: kosmorrolib/dumper.py:258 msgid "Ephemerides of the day" msgstr "" -#: kosmorrolib/dumper.py:260 +#: kosmorrolib/dumper.py:264 msgid "hours" msgstr "" -#: kosmorrolib/dumper.py:264 +#: kosmorrolib/dumper.py:268 msgid "Expected events" msgstr "" -#: kosmorrolib/dumper.py:378 +#: kosmorrolib/dumper.py:382 msgid "" "Building PDFs was not possible, because some dependencies are not " "installed.\n" @@ -204,6 +200,26 @@ msgid "" "information." msgstr "" +#: kosmorrolib/exceptions.py:34 +msgid "The date must be between {minimum_date} and {maximum_date}" +msgstr "" + +#: kosmorrolib/i18n.py:27 +msgid "{day_of_week} {month} {day_number}, {year}" +msgstr "" + +#: kosmorrolib/i18n.py:29 +msgid "{month} {day_number}, {hours}:{minutes}" +msgstr "" + +#: kosmorrolib/i18n.py:31 +msgid "{month} {day_number}, {year}" +msgstr "" + +#: kosmorrolib/i18n.py:32 +msgid "{hours}:{minutes}" +msgstr "" + #: kosmorrolib/main.py:61 msgid "" "Save the planet and paper!\n" @@ -217,87 +233,91 @@ msgid "" "the observation coordinate." msgstr "" -#: kosmorrolib/main.py:98 +#: kosmorrolib/main.py:90 msgid "Could not save the output in \"{path}\": {error}" msgstr "" -#: kosmorrolib/main.py:103 +#: kosmorrolib/main.py:95 msgid "Selected output format needs an output file (--output)." msgstr "" -#: kosmorrolib/main.py:120 +#: kosmorrolib/main.py:112 +msgid "Moon phase can only be displayed between {min_date} and {max_date}" +msgstr "" + +#: kosmorrolib/main.py:134 msgid "Running on Python {python_version}" msgstr "" -#: kosmorrolib/main.py:126 +#: kosmorrolib/main.py:140 msgid "Do you really want to clear Kosmorro's cache? [yN] " msgstr "" -#: kosmorrolib/main.py:133 +#: kosmorrolib/main.py:147 msgid "Answer did not match expected options, cache not cleared." msgstr "" -#: kosmorrolib/main.py:142 +#: kosmorrolib/main.py:156 msgid "" "Compute the ephemerides and the events for a given date, at a given " "position on Earth." msgstr "" -#: kosmorrolib/main.py:144 +#: kosmorrolib/main.py:158 msgid "" "By default, only the events will be computed for today ({date}).\n" "To compute also the ephemerides, latitude and longitude arguments are " "needed." msgstr "" -#: kosmorrolib/main.py:149 +#: kosmorrolib/main.py:163 msgid "Show the program version" msgstr "" -#: kosmorrolib/main.py:151 +#: kosmorrolib/main.py:165 msgid "Delete all the files Kosmorro stored in the cache." msgstr "" -#: kosmorrolib/main.py:153 +#: kosmorrolib/main.py:167 msgid "The format under which the information have to be output" msgstr "" -#: kosmorrolib/main.py:155 +#: kosmorrolib/main.py:169 msgid "" "The observer's latitude on Earth. Can also be set in the " "KOSMORRO_LATITUDE environment variable." msgstr "" -#: kosmorrolib/main.py:158 +#: kosmorrolib/main.py:172 msgid "" "The observer's longitude on Earth. Can also be set in the " "KOSMORRO_LONGITUDE environment variable." msgstr "" -#: kosmorrolib/main.py:161 +#: kosmorrolib/main.py:175 msgid "" "The date for which the ephemerides must be computed (in the YYYY-MM-DD " "format), or as an interval in the \"[+-]YyMmDd\" format (with Y, M, and D" " numbers). Defaults to the current date ({default_date})" msgstr "" -#: kosmorrolib/main.py:166 +#: kosmorrolib/main.py:180 msgid "" "The timezone to display the hours in (e.g. 2 for UTC+2 or -3 for UTC-3). " "Can also be set in the KOSMORRO_TIMEZONE environment variable." msgstr "" -#: kosmorrolib/main.py:169 +#: kosmorrolib/main.py:183 msgid "Disable the colors in the console." msgstr "" -#: kosmorrolib/main.py:171 +#: kosmorrolib/main.py:185 msgid "" "A file to export the output to. If not given, the standard output is " "used. This argument is needed for PDF format." msgstr "" -#: kosmorrolib/main.py:174 +#: kosmorrolib/main.py:188 msgid "" "Do not generate a graph to represent the rise and set times in the PDF " "format." diff --git a/kosmorrolib/main.py b/kosmorrolib/main.py index d649bcb..5cfa960 100644 --- a/kosmorrolib/main.py +++ b/kosmorrolib/main.py @@ -29,7 +29,7 @@ from . import core from . import events from .data import Position, EARTH -from .exceptions import UnavailableFeatureError +from .exceptions import UnavailableFeatureError, OutOfRangeDateError from .i18n import _ from . import ephemerides from .version import VERSION @@ -65,39 +65,31 @@ def main(): print(colored(_("PDF output will not contain the ephemerides, because you didn't provide the observation " "coordinate."), 'yellow')) - try: - timezone = args.timezone - - if timezone is None and environment.timezone is not None: - timezone = int(environment.timezone) - elif timezone is None: - timezone = 0 - - if position is not None: - eph = ephemerides.get_ephemerides(date=compute_date, position=position, timezone=timezone) - else: - eph = None - - moon_phase = ephemerides.get_moon_phase(compute_date) + timezone = args.timezone - events_list = events.search_events(compute_date, timezone) + if timezone is None and environment.timezone is not None: + timezone = int(environment.timezone) + elif timezone is None: + timezone = 0 - format_dumper = output_formats[output_format](ephemerides=eph, moon_phase=moon_phase, events=events_list, - date=compute_date, timezone=timezone, with_colors=args.colors, - show_graph=args.show_graph) - output = format_dumper.to_string() + try: + output = get_information(compute_date, position, timezone, output_format, + args.colors, args.show_graph) except UnavailableFeatureError as error: print(colored(error.msg, 'red')) return 2 + except OutOfRangeDateError as error: + print(colored(error.msg, 'red')) + return 1 if args.output is not None: try: with open(args.output, 'wb') as output_file: - output_file.write(output) + output_file.write(output.to_string()) except OSError as error: print(_('Could not save the output in "{path}": {error}').format(path=args.output, error=error.strerror)) - elif not format_dumper.is_file_output_needed(): + elif not output.is_file_output_needed(): print(output) else: print(colored(_('Selected output format needs an output file (--output).'), color='red')) @@ -106,6 +98,28 @@ def main(): return 0 +def get_information(compute_date: date, position: Position, timezone: int, + output_format: str, colors: bool, show_graph: bool) -> dumper.Dumper: + if position is not None: + eph = ephemerides.get_ephemerides(date=compute_date, position=position, timezone=timezone) + else: + eph = None + + try: + moon_phase = ephemerides.get_moon_phase(compute_date) + except OutOfRangeDateError as error: + moon_phase = None + print(colored(_('Moon phase can only be displayed' + ' between {min_date} and {max_date}').format(min_date=error.min_date, + max_date=error.max_date), 'yellow')) + + events_list = events.search_events(compute_date, timezone) + + return get_dumpers()[output_format](ephemerides=eph, moon_phase=moon_phase, events=events_list, + date=compute_date, timezone=timezone, with_colors=colors, + show_graph=show_graph) + + def get_dumpers() -> {str: dumper.Dumper}: return { 'text': dumper.TextDumper, diff --git a/setup.py b/setup.py index 232dd8f..badbe2f 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ setup( ('man/man1', ['manpage/kosmorro.1']), ('man/man7', ['manpage/kosmorro.7']) ], - install_requires=['skyfield>=1.17.0,<2.0.0', 'tabulate', 'numpy>=1.17.0,<2.0.0', 'termcolor', 'python-dateutil'], + install_requires=['skyfield>=1.21.0,<2.0.0', 'tabulate', 'numpy>=1.17.0,<2.0.0', 'termcolor', 'python-dateutil'], classifiers=[ 'Development Status :: 3 - Alpha', 'Operating System :: POSIX :: Linux', diff --git a/test/dumper.py b/test/dumper.py index 857380a..a6d553e 100644 --- a/test/dumper.py +++ b/test/dumper.py @@ -269,6 +269,10 @@ class DumperTestCase(unittest.TestCase): self._get_events(), date=date(2019, 10, 14)).to_string() self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') + def test_get_moon_with_moon_phase_none(self): + dumper = TextDumper() + self.assertEqual('Moon phase is unavailable for this date.', dumper.get_moon(None)) + @staticmethod def _get_ephemerides(aster_rise_set=False) -> [AsterEphemerides]: rise_time = datetime(2019, 10, 14, 8) if aster_rise_set else None diff --git a/test/ephemerides.py b/test/ephemerides.py index 8eb81ad..278ef57 100644 --- a/test/ephemerides.py +++ b/test/ephemerides.py @@ -3,6 +3,7 @@ from .testutils import expect_assertions from kosmorrolib import ephemerides from kosmorrolib.data import EARTH, Position, MoonPhase from datetime import date +from kosmorrolib.exceptions import OutOfRangeDateError class EphemeridesTestCase(unittest.TestCase): @@ -107,6 +108,14 @@ class EphemeridesTestCase(unittest.TestCase): phase = MoonPhase('WANING_CRESCENT', None, None) self.assertEqual('New Moon', phase.get_next_phase_name()) + def test_get_ephemerides_raises_exception_on_out_of_date_range(self): + with self.assertRaises(OutOfRangeDateError): + ephemerides.get_ephemerides(date(1789, 5, 5), Position(0, 0, EARTH)) + + def test_get_moon_phase_raises_exception_on_out_of_date_range(self): + with self.assertRaises(OutOfRangeDateError): + ephemerides.get_moon_phase(date(1789, 5, 5)) + if __name__ == '__main__': unittest.main() diff --git a/test/events.py b/test/events.py index 37db791..fc78e88 100644 --- a/test/events.py +++ b/test/events.py @@ -6,6 +6,7 @@ from kosmorrolib import events from kosmorrolib.data import Event, ASTERS from kosmorrolib.core import get_timescale from unittest_data_provider import data_provider +from kosmorrolib.exceptions import OutOfRangeDateError class EventTestCase(unittest.TestCase): @@ -61,6 +62,10 @@ class EventTestCase(unittest.TestCase): self.assertEqual(expected_event.__dict__, actual_event.__dict__) + def test_get_events_raises_exception_on_out_of_date_range(self): + with self.assertRaises(OutOfRangeDateError): + events.search_events(date(1789, 5, 5)) + if __name__ == '__main__': unittest.main()