| @@ -145,7 +145,8 @@ disable=print-statement, | |||
| too-many-locals, | |||
| too-many-branches, | |||
| too-few-public-methods, | |||
| protected-access | |||
| protected-access, | |||
| unnecessary-comprehension | |||
| # Enable the message, report, category or checker with the given id(s). You can | |||
| # either give multiple identifier separated by comma (,) or put this option | |||
| @@ -24,6 +24,7 @@ from kosmorrolib.version import VERSION | |||
| from kosmorrolib import dumper | |||
| from kosmorrolib import core | |||
| from kosmorrolib.ephemerides import EphemeridesComputer, Position | |||
| from kosmorrolib import events | |||
| def main(): | |||
| @@ -37,13 +38,17 @@ def main(): | |||
| month = args.month | |||
| day = args.day | |||
| compute_date = date(year, month, day) | |||
| if day is not None and month is None: | |||
| month = date.today().month | |||
| ephemeris = EphemeridesComputer(Position(args.latitude, args.longitude)) | |||
| ephemerides = ephemeris.compute_ephemerides(year, month, day) | |||
| dump = output_formats[args.format](ephemerides, date(year, month, day)) | |||
| events_list = events.search_events(compute_date) | |||
| dump = output_formats[args.format](ephemerides, events_list, compute_date) | |||
| print(dump.to_string()) | |||
| return 0 | |||
| @@ -33,6 +33,10 @@ MOON_PHASES = { | |||
| 'WANING_CRESCENT': 'Waning crescent' | |||
| } | |||
| EVENTS = { | |||
| 'OPPOSITION': {'message': '%s is in opposition'} | |||
| } | |||
| class MoonPhase: | |||
| def __init__(self, identifier: str, time: Union[Time, None], next_phase_date: Union[Time, None]): | |||
| @@ -131,3 +135,20 @@ class DwarfPlanet(Planet): | |||
| class Satellite(Object): | |||
| def get_type(self) -> str: | |||
| return 'satellite' | |||
| class Event: | |||
| def __init__(self, event_type: str, aster: [Object], start_time: Time, end_time: Union[Time, None] = None): | |||
| if event_type not in EVENTS.keys(): | |||
| raise ValueError('event_type parameter must be one of the following: %s (got %s)' % ( | |||
| ', '.join(EVENTS.keys()), | |||
| event_type) | |||
| ) | |||
| self.event_type = event_type | |||
| self.object = aster | |||
| self.start_time = start_time | |||
| self.end_time = end_time | |||
| def get_description(self) -> str: | |||
| return EVENTS[self.event_type]['message'] % self.object.name | |||
| @@ -22,12 +22,13 @@ import json | |||
| from tabulate import tabulate | |||
| from skyfield.timelib import Time | |||
| from numpy import int64 | |||
| from .data import Object, AsterEphemerides, MoonPhase | |||
| from .data import Object, AsterEphemerides, MoonPhase, Event | |||
| class Dumper(ABC): | |||
| def __init__(self, ephemeris: dict, date: datetime.date = datetime.date.today()): | |||
| def __init__(self, ephemeris: dict, events: [Event], date: datetime.date = datetime.date.today()): | |||
| self.ephemeris = ephemeris | |||
| self.events = events | |||
| self.date = date | |||
| @abstractmethod | |||
| @@ -37,6 +38,7 @@ class Dumper(ABC): | |||
| class JsonDumper(Dumper): | |||
| def to_string(self): | |||
| self.ephemeris['events'] = self.events | |||
| return json.dumps(self.ephemeris, | |||
| default=self._json_default, | |||
| indent=4) | |||
| @@ -60,16 +62,31 @@ class JsonDumper(Dumper): | |||
| moon_phase['phase'] = moon_phase.pop('identifier') | |||
| moon_phase['date'] = moon_phase.pop('time') | |||
| return moon_phase | |||
| if isinstance(obj, Event): | |||
| event = obj.__dict__ | |||
| event['object'] = event['object'].name | |||
| return event | |||
| raise TypeError('Object of type "%s" could not be integrated in the JSON' % str(type(obj))) | |||
| class TextDumper(Dumper): | |||
| def to_string(self): | |||
| return '\n\n'.join(['Ephemerides of %s' % self.date.strftime('%A %B %d, %Y'), | |||
| text = 'Ephemerides of %s' % self.date.strftime('%A %B %d, %Y') | |||
| text = '\n\n'.join([text, | |||
| self.get_asters(self.ephemeris['details']), | |||
| self.get_moon(self.ephemeris['moon_phase']), | |||
| 'Note: All the hours are given in UTC.']) | |||
| self.get_moon(self.ephemeris['moon_phase']) | |||
| ]) | |||
| if len(self.events) > 0: | |||
| text = '\n\n'.join([text, | |||
| 'Expected events:', | |||
| self.get_events(self.events) | |||
| ]) | |||
| text = '\n\n'.join([text, 'Note: All the hours are given in UTC.']) | |||
| return text | |||
| @staticmethod | |||
| def get_asters(asters: [Object]) -> str: | |||
| @@ -98,6 +115,15 @@ class TextDumper(Dumper): | |||
| return tabulate(data, headers=['Object', 'Rise time', 'Culmination time', 'Set time'], tablefmt='simple', | |||
| stralign='center', colalign=('left',)) | |||
| @staticmethod | |||
| def get_events(events: [Event]) -> str: | |||
| data = [] | |||
| for event in events: | |||
| data.append([event.start_time.utc_strftime('%H:%M'), event.get_description()]) | |||
| return tabulate(data, tablefmt='plain', stralign='left') | |||
| @staticmethod | |||
| def get_moon(moon_phase: MoonPhase) -> str: | |||
| return 'Moon phase: %s\n' \ | |||
| @@ -0,0 +1,61 @@ | |||
| #!/usr/bin/env python3 | |||
| # Kosmorro - Compute The Next Ephemerides | |||
| # Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
| # | |||
| # This program is free software: you can redistribute it and/or modify | |||
| # it under the terms of the GNU Affero General Public License as | |||
| # published by the Free Software Foundation, either version 3 of the | |||
| # License, or (at your option) any later version. | |||
| # | |||
| # This program is distributed in the hope that it will be useful, | |||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| # GNU Affero General Public License for more details. | |||
| # | |||
| # You should have received a copy of the GNU Affero General Public License | |||
| # along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
| from datetime import date as date_type | |||
| from skyfield.timelib import Time | |||
| from skyfield.almanac import find_discrete | |||
| from .data import Event, Planet | |||
| from .core import get_timescale, get_skf_objects, ASTERS | |||
| def _search_oppositions(start_time: Time, end_time: Time) -> [Event]: | |||
| earth = get_skf_objects()['earth'] | |||
| sun = get_skf_objects()['sun'] | |||
| aster = None | |||
| def is_oppositing(time: Time) -> [bool]: | |||
| earth_pos = earth.at(time) | |||
| sun_pos = earth_pos.observe(sun).apparent() # Never do this without eyes protection! | |||
| aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent() | |||
| _, lon1, _ = sun_pos.ecliptic_latlon() | |||
| _, lon2, _ = aster_pos.ecliptic_latlon() | |||
| return (lon1.degrees - lon2.degrees) > 180 | |||
| is_oppositing.rough_period = 1.0 | |||
| events = [] | |||
| for aster in ASTERS: | |||
| if not isinstance(aster, Planet) or aster.name in ['Mercury', 'Venus']: | |||
| continue | |||
| times, _ = find_discrete(start_time, end_time, is_oppositing) | |||
| for time in times: | |||
| events.append(Event('OPPOSITION', aster, time)) | |||
| return events | |||
| def search_events(date: date_type) -> [Event]: | |||
| start_time = get_timescale().utc(date.year, date.month, date.day) | |||
| end_time = get_timescale().utc(date.year, date.month, date.day + 1) | |||
| return [ | |||
| opposition for opposition in _search_oppositions(start_time, end_time) | |||
| ] | |||
| @@ -1,2 +1,3 @@ | |||
| from .dumper import * | |||
| from .ephemerides import * | |||
| from .events import * | |||
| @@ -1,17 +1,22 @@ | |||
| import unittest | |||
| from kosmorrolib.data import AsterEphemerides, Planet, MoonPhase | |||
| from kosmorrolib.dumper import JsonDumper | |||
| from datetime import date | |||
| from kosmorrolib.data import AsterEphemerides, Planet, MoonPhase, Event | |||
| from kosmorrolib.dumper import JsonDumper, TextDumper | |||
| from kosmorrolib.core import get_timescale | |||
| class DumperTestCase(unittest.TestCase): | |||
| def setUp(self) -> None: | |||
| self.maxDiff = None | |||
| def test_json_dumper_returns_correct_json(self): | |||
| data = self._get_data() | |||
| self.assertEqual('{\n' | |||
| ' "moon_phase": {\n' | |||
| ' "next_phase_date": "2019-11-20T00:00:00Z",\n' | |||
| ' "next_phase_date": "2019-10-21T00:00:00Z",\n' | |||
| ' "phase": "FULL_MOON",\n' | |||
| ' "date": "2019-11-11T00:00:00Z"\n' | |||
| ' "date": "2019-10-14T00:00:00Z"\n' | |||
| ' },\n' | |||
| ' "details": [\n' | |||
| ' {\n' | |||
| @@ -22,13 +27,51 @@ class DumperTestCase(unittest.TestCase): | |||
| ' "set_time": null\n' | |||
| ' }\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "events": [\n' | |||
| ' {\n' | |||
| ' "event_type": "OPPOSITION",\n' | |||
| ' "object": "Mars",\n' | |||
| ' "start_time": "2018-07-27T05:12:00Z",\n' | |||
| ' "end_time": null\n' | |||
| ' }\n' | |||
| ' ]\n' | |||
| '}', JsonDumper(data).to_string()) | |||
| '}', JsonDumper(data, | |||
| [Event('OPPOSITION', Planet('Mars', 'MARS'), | |||
| get_timescale().utc(2018, 7, 27, 5, 12))] | |||
| ).to_string()) | |||
| def test_text_dumper_without_events(self): | |||
| ephemerides = self._get_data() | |||
| self.assertEqual('Ephemerides of Monday October 14, 2019\n\n' | |||
| 'Object Rise time Culmination time Set time\n' | |||
| '-------- ----------- ------------------ ----------\n' | |||
| 'Mars - - -\n\n' | |||
| 'Moon phase: Full Moon\n' | |||
| 'Last Quarter on Mon Oct 21, 2019 00:00\n\n' | |||
| 'Note: All the hours are given in UTC.', | |||
| TextDumper(ephemerides, [], date=date(2019, 10, 14)).to_string()) | |||
| def test_text_dumper_with_events(self): | |||
| ephemerides = self._get_data() | |||
| self.assertEqual('Ephemerides of Monday October 14, 2019\n\n' | |||
| 'Object Rise time Culmination time Set time\n' | |||
| '-------- ----------- ------------------ ----------\n' | |||
| 'Mars - - -\n\n' | |||
| 'Moon phase: Full Moon\n' | |||
| 'Last Quarter on Mon Oct 21, 2019 00:00\n\n' | |||
| 'Expected events:\n\n' | |||
| '05:12 Mars is in opposition\n\n' | |||
| 'Note: All the hours are given in UTC.', | |||
| TextDumper(ephemerides, [Event('OPPOSITION', | |||
| Planet('Mars', 'MARS'), | |||
| get_timescale().utc(2018, 7, 27, 5, 12)) | |||
| ], date=date(2019, 10, 14)).to_string()) | |||
| @staticmethod | |||
| def _get_data(): | |||
| return { | |||
| 'moon_phase': MoonPhase('FULL_MOON', get_timescale().utc(2019, 11, 11), get_timescale().utc(2019, 11, 20)), | |||
| 'moon_phase': MoonPhase('FULL_MOON', get_timescale().utc(2019, 10, 14), get_timescale().utc(2019, 10, 21)), | |||
| 'details': [Planet('Mars', 'MARS', AsterEphemerides(None, None, None))] | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| import unittest | |||
| from datetime import date | |||
| from kosmorrolib import events | |||
| from kosmorrolib.data import Event | |||
| from kosmorrolib.core import get_timescale | |||
| class MyTestCase(unittest.TestCase): | |||
| def test_event_only_accepts_valid_values(self): | |||
| with self.assertRaises(ValueError): | |||
| Event('SUPERNOVA', None, get_timescale().now()) | |||
| def test_find_oppositions(self): | |||
| # Test case: Mars opposition | |||
| # Source of the information: https://promenade.imcce.fr/en/pages6/887.html#mar | |||
| o1 = (events.search_events(date(2020, 10, 13)), '^2020-10-13T23:25') | |||
| o2 = (events.search_events(date(2022, 12, 8)), '^2022-12-08T05:41') | |||
| o3 = (events.search_events(date(2025, 1, 16)), '^2025-01-16T02:38') | |||
| o4 = (events.search_events(date(2027, 2, 19)), '^2027-02-19T15:50') | |||
| for (o, expected_date) in [o1, o2, o3, o4]: | |||
| self.assertEqual(1, len(o), 'Expected 1 event for %s, got %d' % (expected_date, len(o))) | |||
| self.assertEqual('OPPOSITION', o[0].event_type) | |||
| self.assertEqual('MARS', o[0].object.skyfield_name) | |||
| self.assertRegex(o[0].start_time.utc_iso(), expected_date) | |||
| self.assertIsNone(o[0].end_time) | |||
| self.assertEqual('Mars is in opposition', o[0].get_description()) | |||
| if __name__ == '__main__': | |||
| unittest.main() | |||