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 0000000..deff19a Binary files /dev/null and b/kosmorrolib/assets/moonphases/png/unknown.png differ diff --git a/kosmorrolib/assets/moonphases/svg/unknown.svg b/kosmorrolib/assets/moonphases/svg/unknown.svg new file mode 100644 index 0000000..3adc8c4 --- /dev/null +++ b/kosmorrolib/assets/moonphases/svg/unknown.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + 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()