refactor: simplify ephemerides, remove dead codetags/v0.8.0
| @@ -47,7 +47,13 @@ EVENTS = { | |||
| } | |||
| class MoonPhase: | |||
| class Serializable(ABC): | |||
| @abstractmethod | |||
| def serialize(self) -> dict: | |||
| pass | |||
| class MoonPhase(Serializable): | |||
| def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[datetime, None]): | |||
| if identifier not in MOON_PHASES.keys(): | |||
| raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()), | |||
| @@ -60,6 +66,11 @@ class MoonPhase: | |||
| def get_phase(self): | |||
| return MOON_PHASES[self.identifier] | |||
| def get_next_phase_name(self): | |||
| next_identifier = self.get_next_phase() | |||
| return MOON_PHASES[next_identifier] | |||
| def get_next_phase(self): | |||
| if self.identifier == 'NEW_MOON' or self.identifier == 'WAXING_CRESCENT': | |||
| next_identifier = 'FIRST_QUARTER' | |||
| @@ -69,39 +80,20 @@ class MoonPhase: | |||
| next_identifier = 'LAST_QUARTER' | |||
| else: | |||
| next_identifier = 'NEW_MOON' | |||
| return next_identifier | |||
| return MOON_PHASES[next_identifier] | |||
| class Position: | |||
| def __init__(self, latitude: float, longitude: float): | |||
| self.latitude = latitude | |||
| self.longitude = longitude | |||
| self.observation_planet = None | |||
| self._topos = None | |||
| def get_planet_topos(self) -> Topos: | |||
| if self.observation_planet is None: | |||
| raise TypeError('Observation planet must be set.') | |||
| if self._topos is None: | |||
| self._topos = self.observation_planet + Topos(latitude_degrees=self.latitude, | |||
| longitude_degrees=self.longitude) | |||
| return self._topos | |||
| class AsterEphemerides: | |||
| def __init__(self, | |||
| rise_time: Union[datetime, None], | |||
| culmination_time: Union[datetime, None], | |||
| set_time: Union[datetime, None]): | |||
| self.rise_time = rise_time | |||
| self.culmination_time = culmination_time | |||
| self.set_time = set_time | |||
| def serialize(self) -> dict: | |||
| return { | |||
| 'phase': self.identifier, | |||
| 'time': self.time.isoformat() if self.time is not None else None, | |||
| 'next': { | |||
| 'phase': self.get_next_phase(), | |||
| 'time': self.next_phase_date.isoformat() | |||
| } | |||
| } | |||
| class Object(ABC): | |||
| class Object(Serializable): | |||
| """ | |||
| An astronomical object. | |||
| """ | |||
| @@ -109,7 +101,6 @@ class Object(ABC): | |||
| def __init__(self, | |||
| name: str, | |||
| skyfield_name: str, | |||
| ephemerides: AsterEphemerides or None = None, | |||
| radius: float = None): | |||
| """ | |||
| Initialize an astronomical object | |||
| @@ -122,7 +113,6 @@ class Object(ABC): | |||
| self.name = name | |||
| self.skyfield_name = skyfield_name | |||
| self.radius = radius | |||
| self.ephemerides = ephemerides | |||
| def get_skyfield_object(self) -> SkfPlanet: | |||
| return get_skf_objects()[self.skyfield_name] | |||
| @@ -143,6 +133,13 @@ class Object(ABC): | |||
| return 360 / pi * arcsin(self.radius / from_place.at(time).observe(self.get_skyfield_object()).distance().km) | |||
| def serialize(self) -> dict: | |||
| return { | |||
| 'name': self.name, | |||
| 'type': self.get_type(), | |||
| 'radius': self.radius, | |||
| } | |||
| class Star(Object): | |||
| def get_type(self) -> str: | |||
| @@ -164,7 +161,7 @@ class Satellite(Object): | |||
| return 'satellite' | |||
| class Event: | |||
| class Event(Serializable): | |||
| def __init__(self, event_type: str, objects: [Object], start_time: datetime, | |||
| end_time: Union[datetime, None] = None, details: str = None): | |||
| if event_type not in EVENTS.keys(): | |||
| @@ -190,6 +187,15 @@ class Event: | |||
| return tuple(object.name for object in self.objects) | |||
| def serialize(self) -> dict: | |||
| return { | |||
| 'objects': [object.serialize() for object in self.objects], | |||
| 'event': self.event_type, | |||
| 'starts_at': self.start_time.isoformat(), | |||
| 'ends_at': self.end_time.isoformat() if self.end_time is not None else None, | |||
| 'details': self.details | |||
| } | |||
| def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonPhase, None]: | |||
| tomorrow = get_timescale().utc(now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1) | |||
| @@ -228,8 +234,30 @@ def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonP | |||
| next_phase_time.utc_datetime() if next_phase_time is not None else None) | |||
| class AsterEphemerides(Serializable): | |||
| def __init__(self, | |||
| rise_time: Union[datetime, None], | |||
| culmination_time: Union[datetime, None], | |||
| set_time: Union[datetime, None], | |||
| aster: Object): | |||
| self.rise_time = rise_time | |||
| self.culmination_time = culmination_time | |||
| self.set_time = set_time | |||
| self.object = aster | |||
| def serialize(self) -> dict: | |||
| return { | |||
| 'object': self.object.serialize(), | |||
| 'rise_time': self.rise_time.isoformat() if self.rise_time is not None else None, | |||
| 'culmination_time': self.culmination_time.isoformat() if self.culmination_time is not None else None, | |||
| 'set_time': self.set_time.isoformat() if self.set_time is not None else None | |||
| } | |||
| MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] | |||
| EARTH = Planet('Earth', 'EARTH') | |||
| ASTERS = [Star(_('Sun'), 'SUN', radius=696342), | |||
| Satellite(_('Moon'), 'MOON', radius=1737.4), | |||
| Planet(_('Mercury'), 'MERCURY', radius=2439.7), | |||
| @@ -240,3 +268,21 @@ ASTERS = [Star(_('Sun'), 'SUN', radius=696342), | |||
| Planet(_('Uranus'), 'URANUS BARYCENTER', radius=25559), | |||
| Planet(_('Neptune'), 'NEPTUNE BARYCENTER', radius=24764), | |||
| Planet(_('Pluto'), 'PLUTO BARYCENTER', radius=1185)] | |||
| class Position: | |||
| def __init__(self, latitude: float, longitude: float, aster: Object): | |||
| self.latitude = latitude | |||
| self.longitude = longitude | |||
| self.aster = aster | |||
| self._topos = None | |||
| def get_planet_topos(self) -> Topos: | |||
| if self.aster is None: | |||
| raise TypeError('Observation planet must be set.') | |||
| if self._topos is None: | |||
| self._topos = self.aster.get_skyfield_object() + Topos(latitude_degrees=self.latitude, | |||
| longitude_degrees=self.longitude) | |||
| return self._topos | |||
| @@ -40,9 +40,10 @@ TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M') | |||
| class Dumper(ABC): | |||
| def __init__(self, ephemeris: dict, events: [Event], date: datetime.date = datetime.date.today(), timezone: int = 0, | |||
| with_colors: bool = True): | |||
| self.ephemeris = ephemeris | |||
| def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None, | |||
| date: datetime.date = datetime.date.today(), timezone: int = 0, with_colors: bool = True): | |||
| self.ephemerides = ephemerides | |||
| self.moon_phase = moon_phase | |||
| self.events = events | |||
| self.date = date | |||
| self.timezone = timezone | |||
| @@ -52,19 +53,20 @@ class Dumper(ABC): | |||
| self._convert_dates_to_timezones() | |||
| def _convert_dates_to_timezones(self): | |||
| if self.ephemeris['moon_phase'].time is not None: | |||
| self.ephemeris['moon_phase'].time = self._datetime_to_timezone(self.ephemeris['moon_phase'].time) | |||
| if self.ephemeris['moon_phase'].next_phase_date is not None: | |||
| self.ephemeris['moon_phase'].next_phase_date = self._datetime_to_timezone( | |||
| self.ephemeris['moon_phase'].next_phase_date) | |||
| for aster in self.ephemeris['details']: | |||
| if aster.ephemerides.rise_time is not None: | |||
| aster.ephemerides.rise_time = self._datetime_to_timezone(aster.ephemerides.rise_time) | |||
| if aster.ephemerides.culmination_time is not None: | |||
| aster.ephemerides.culmination_time = self._datetime_to_timezone(aster.ephemerides.culmination_time) | |||
| if aster.ephemerides.set_time is not None: | |||
| aster.ephemerides.set_time = self._datetime_to_timezone(aster.ephemerides.set_time) | |||
| if self.moon_phase.time is not None: | |||
| self.moon_phase.time = self._datetime_to_timezone(self.moon_phase.time) | |||
| if self.moon_phase.next_phase_date is not None: | |||
| self.moon_phase.next_phase_date = self._datetime_to_timezone( | |||
| self.moon_phase.next_phase_date) | |||
| if self.ephemerides is not None: | |||
| for ephemeris in self.ephemerides: | |||
| if ephemeris.rise_time is not None: | |||
| ephemeris.rise_time = self._datetime_to_timezone(ephemeris.rise_time) | |||
| if ephemeris.culmination_time is not None: | |||
| ephemeris.culmination_time = self._datetime_to_timezone(ephemeris.culmination_time) | |||
| if ephemeris.set_time is not None: | |||
| ephemeris.set_time = self._datetime_to_timezone(ephemeris.set_time) | |||
| for event in self.events: | |||
| event.start_time = self._datetime_to_timezone(event.start_time) | |||
| @@ -99,11 +101,11 @@ class Dumper(ABC): | |||
| class JsonDumper(Dumper): | |||
| def to_string(self): | |||
| self.ephemeris['events'] = self.events | |||
| self.ephemeris['ephemerides'] = self.ephemeris.pop('details') | |||
| return json.dumps(self.ephemeris, | |||
| default=self._json_default, | |||
| indent=4) | |||
| return json.dumps({ | |||
| 'ephemerides': [ephemeris.serialize() for ephemeris in self.ephemerides], | |||
| 'moon_phase': self.moon_phase.serialize(), | |||
| 'events': [event.serialize() for event in self.events] | |||
| }, indent=4) | |||
| @staticmethod | |||
| def _json_default(obj): | |||
| @@ -139,10 +141,10 @@ class TextDumper(Dumper): | |||
| def to_string(self): | |||
| text = [self.style(self.get_date_as_string(capitalized=True), 'h1')] | |||
| if len(self.ephemeris['details']) > 0: | |||
| text.append(self.get_asters(self.ephemeris['details'])) | |||
| if self.ephemerides is not None: | |||
| text.append(self.stringify_ephemerides()) | |||
| text.append(self.get_moon(self.ephemeris['moon_phase'])) | |||
| text.append(self.get_moon(self.moon_phase)) | |||
| if len(self.events) > 0: | |||
| text.append('\n'.join([self.style(_('Expected events:'), 'h2'), | |||
| @@ -173,28 +175,28 @@ class TextDumper(Dumper): | |||
| return styles[tag](text) | |||
| def get_asters(self, asters: [Object]) -> str: | |||
| def stringify_ephemerides(self) -> str: | |||
| data = [] | |||
| for aster in asters: | |||
| name = self.style(aster.name, 'th') | |||
| for ephemeris in self.ephemerides: | |||
| name = self.style(ephemeris.object.name, 'th') | |||
| if aster.ephemerides.rise_time is not None: | |||
| time_fmt = TIME_FORMAT if aster.ephemerides.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
| planet_rise = aster.ephemerides.rise_time.strftime(time_fmt) | |||
| if ephemeris.rise_time is not None: | |||
| time_fmt = TIME_FORMAT if ephemeris.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
| planet_rise = ephemeris.rise_time.strftime(time_fmt) | |||
| else: | |||
| planet_rise = '-' | |||
| if aster.ephemerides.culmination_time is not None: | |||
| time_fmt = TIME_FORMAT if aster.ephemerides.culmination_time.day == self.date.day \ | |||
| if ephemeris.culmination_time is not None: | |||
| time_fmt = TIME_FORMAT if ephemeris.culmination_time.day == self.date.day \ | |||
| else SHORT_DATETIME_FORMAT | |||
| planet_culmination = aster.ephemerides.culmination_time.strftime(time_fmt) | |||
| planet_culmination = ephemeris.culmination_time.strftime(time_fmt) | |||
| else: | |||
| planet_culmination = '-' | |||
| if aster.ephemerides.set_time is not None: | |||
| time_fmt = TIME_FORMAT if aster.ephemerides.set_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
| planet_set = aster.ephemerides.set_time.strftime(time_fmt) | |||
| if ephemeris.set_time is not None: | |||
| time_fmt = TIME_FORMAT if ephemeris.set_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
| planet_set = ephemeris.set_time.strftime(time_fmt) | |||
| else: | |||
| planet_set = '-' | |||
| @@ -219,7 +221,7 @@ class TextDumper(Dumper): | |||
| def get_moon(self, moon_phase: MoonPhase) -> str: | |||
| 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(), | |||
| next_moon_phase=moon_phase.get_next_phase_name(), | |||
| next_moon_phase_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT), | |||
| next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_FORMAT) | |||
| ) | |||
| @@ -242,12 +244,12 @@ class _LatexDumper(Dumper): | |||
| 'assets', 'png', 'kosmorro-logo.png') | |||
| moon_phase_graphics = os.path.join(os.path.abspath(os.path.dirname(__file__)), | |||
| 'assets', 'moonphases', 'png', | |||
| '.'.join([self.ephemeris['moon_phase'].identifier.lower().replace('_', '-'), | |||
| '.'.join([self.moon_phase.identifier.lower().replace('_', '-'), | |||
| 'png'])) | |||
| document = template | |||
| if len(self.ephemeris['details']) == 0: | |||
| if self.ephemerides is None: | |||
| document = self._remove_section(document, 'ephemerides') | |||
| if len(self.events) == 0: | |||
| @@ -276,7 +278,7 @@ class _LatexDumper(Dumper): | |||
| .replace('+++EPHEMERIDES+++', self._make_ephemerides()) \ | |||
| .replace('+++MOON-PHASE-GRAPHICS+++', moon_phase_graphics) \ | |||
| .replace('+++CURRENT-MOON-PHASE-TITLE+++', _('Moon phase:')) \ | |||
| .replace('+++CURRENT-MOON-PHASE+++', self.ephemeris['moon_phase'].get_phase()) \ | |||
| .replace('+++CURRENT-MOON-PHASE+++', self.moon_phase.get_phase()) \ | |||
| .replace('+++SECTION-EVENTS+++', _('Expected events')) \ | |||
| .replace('+++EVENTS+++', self._make_events()) | |||
| @@ -285,30 +287,31 @@ class _LatexDumper(Dumper): | |||
| def _make_ephemerides(self) -> str: | |||
| latex = [] | |||
| for aster in self.ephemeris['details']: | |||
| if aster.ephemerides.rise_time is not None: | |||
| time_fmt = TIME_FORMAT if aster.ephemerides.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
| aster_rise = aster.ephemerides.rise_time.strftime(time_fmt) | |||
| else: | |||
| aster_rise = '-' | |||
| if aster.ephemerides.culmination_time is not None: | |||
| time_fmt = TIME_FORMAT if aster.ephemerides.culmination_time.day == self.date.day\ | |||
| else SHORT_DATETIME_FORMAT | |||
| aster_culmination = aster.ephemerides.culmination_time.strftime(time_fmt) | |||
| else: | |||
| aster_culmination = '-' | |||
| if aster.ephemerides.set_time is not None: | |||
| time_fmt = TIME_FORMAT if aster.ephemerides.set_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
| aster_set = aster.ephemerides.set_time.strftime(time_fmt) | |||
| else: | |||
| aster_set = '-' | |||
| latex.append(r'\object{%s}{%s}{%s}{%s}' % (aster.name, | |||
| aster_rise, | |||
| aster_culmination, | |||
| aster_set)) | |||
| if self.ephemerides is not None: | |||
| for ephemeris in self.ephemerides: | |||
| if ephemeris.rise_time is not None: | |||
| time_fmt = TIME_FORMAT if ephemeris.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
| aster_rise = ephemeris.rise_time.strftime(time_fmt) | |||
| else: | |||
| aster_rise = '-' | |||
| if ephemeris.culmination_time is not None: | |||
| time_fmt = TIME_FORMAT if ephemeris.culmination_time.day == self.date.day\ | |||
| else SHORT_DATETIME_FORMAT | |||
| aster_culmination = ephemeris.culmination_time.strftime(time_fmt) | |||
| else: | |||
| aster_culmination = '-' | |||
| if ephemeris.set_time is not None: | |||
| time_fmt = TIME_FORMAT if ephemeris.set_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
| aster_set = ephemeris.set_time.strftime(time_fmt) | |||
| else: | |||
| aster_set = '-' | |||
| latex.append(r'\object{%s}{%s}{%s}{%s}' % (ephemeris.object.name, | |||
| aster_rise, | |||
| aster_culmination, | |||
| aster_set)) | |||
| return ''.join(latex) | |||
| @@ -342,13 +345,9 @@ class _LatexDumper(Dumper): | |||
| class PdfDumper(Dumper): | |||
| def __init__(self, ephemerides, events, date=datetime.datetime.now(), timezone=0, with_colors=True): | |||
| super(PdfDumper, self).__init__(ephemerides, events, date=date, timezone=0, with_colors=with_colors) | |||
| self.timezone = timezone | |||
| def to_string(self): | |||
| try: | |||
| latex_dumper = _LatexDumper(self.ephemeris, self.events, | |||
| latex_dumper = _LatexDumper(self.ephemerides, self.moon_phase, self.events, | |||
| date=self.date, timezone=self.timezone, with_colors=self.with_colors) | |||
| return self._compile(latex_dumper.to_string()) | |||
| except RuntimeError: | |||
| @@ -17,77 +17,63 @@ | |||
| # along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
| import datetime | |||
| from typing import Union | |||
| from skyfield import almanac | |||
| from skyfield.searchlib import find_discrete, find_maxima | |||
| from skyfield.timelib import Time | |||
| from skyfield.constants import tau | |||
| from .data import Object, Position, AsterEphemerides, MoonPhase, ASTERS, skyfield_to_moon_phase | |||
| from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS, skyfield_to_moon_phase | |||
| from .core import get_skf_objects, get_timescale, get_iau2000b | |||
| RISEN_ANGLE = -0.8333 | |||
| class EphemeridesComputer: | |||
| def __init__(self, position: Union[Position, None]): | |||
| if position is not None: | |||
| position.observation_planet = get_skf_objects()['earth'] | |||
| self.position = position | |||
| def get_moon_phase(compute_date: datetime.date) -> MoonPhase: | |||
| earth = get_skf_objects()['earth'] | |||
| moon = get_skf_objects()['moon'] | |||
| sun = get_skf_objects()['sun'] | |||
| def get_sun(self, start_time, end_time) -> dict: | |||
| times, is_risen = find_discrete(start_time, | |||
| end_time, | |||
| almanac.sunrise_sunset(get_skf_objects(), self.position)) | |||
| def moon_phase_at(time: Time): | |||
| time._nutation_angles = get_iau2000b(time) | |||
| current_earth = earth.at(time) | |||
| _, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date') | |||
| _, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date') | |||
| return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) | |||
| sunrise = times[0] if is_risen[0] else times[1] | |||
| sunset = times[1] if not is_risen[1] else times[0] | |||
| moon_phase_at.rough_period = 7.0 # one lunar phase per week | |||
| return {'rise': sunrise, 'set': sunset} | |||
| today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day) | |||
| 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) | |||
| @staticmethod | |||
| def get_moon_phase(compute_date: datetime.date) -> MoonPhase: | |||
| earth = get_skf_objects()['earth'] | |||
| moon = get_skf_objects()['moon'] | |||
| sun = get_skf_objects()['sun'] | |||
| times, phase = find_discrete(time1, time2, moon_phase_at) | |||
| def moon_phase_at(time: Time): | |||
| time._nutation_angles = get_iau2000b(time) | |||
| current_earth = earth.at(time) | |||
| _, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date') | |||
| _, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date') | |||
| return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) | |||
| return skyfield_to_moon_phase(times, phase, today) | |||
| moon_phase_at.rough_period = 7.0 # one lunar phase per week | |||
| today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day) | |||
| 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) | |||
| def get_ephemerides(date: datetime.date, position: Position) -> [AsterEphemerides]: | |||
| ephemerides = [] | |||
| times, phase = find_discrete(time1, time2, moon_phase_at) | |||
| def get_angle(for_aster: Object): | |||
| def fun(time: Time) -> float: | |||
| return position.get_planet_topos().at(time).observe(for_aster.get_skyfield_object()).apparent().altaz()[0]\ | |||
| .degrees | |||
| fun.rough_period = 1.0 | |||
| return fun | |||
| return skyfield_to_moon_phase(times, phase, today) | |||
| def is_risen(for_aster: Object): | |||
| def fun(time: Time) -> bool: | |||
| return get_angle(for_aster)(time) > RISEN_ANGLE | |||
| fun.rough_period = 0.5 | |||
| return fun | |||
| @staticmethod | |||
| def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: | |||
| skyfield_aster = get_skf_objects()[aster.skyfield_name] | |||
| start_time = get_timescale().utc(date.year, date.month, date.day) | |||
| end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59) | |||
| def get_angle(time: Time) -> float: | |||
| return position.get_planet_topos().at(time).observe(skyfield_aster).apparent().altaz()[0].degrees | |||
| def is_risen(time: Time) -> bool: | |||
| return get_angle(time) > RISEN_ANGLE | |||
| get_angle.rough_period = 1.0 | |||
| is_risen.rough_period = 0.5 | |||
| start_time = get_timescale().utc(date.year, date.month, date.day) | |||
| end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59) | |||
| rise_times, arr = find_discrete(start_time, end_time, is_risen) | |||
| 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, epsilon=1./3600/24, num=12) | |||
| 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 | |||
| @@ -109,37 +95,6 @@ class EphemeridesComputer: | |||
| if set_time is not None: | |||
| set_time = set_time.utc_datetime().replace(microsecond=0) if set_time is not None else None | |||
| aster.ephemerides = AsterEphemerides(rise_time, culmination_time, set_time) | |||
| return aster | |||
| @staticmethod | |||
| def is_leap_year(year: int) -> bool: | |||
| return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) | |||
| def compute_ephemerides(self, compute_date: datetime.date) -> dict: | |||
| return {'moon_phase': self.get_moon_phase(compute_date), | |||
| 'details': [self.get_asters_ephemerides_for_aster(aster, compute_date, self.position) | |||
| for aster in ASTERS] if self.position is not None else []} | |||
| @staticmethod | |||
| def get_seasons(year: int) -> dict: | |||
| start_time = get_timescale().utc(year, 1, 1) | |||
| end_time = get_timescale().utc(year, 12, 31) | |||
| times, almanac_seasons = find_discrete(start_time, end_time, almanac.seasons(get_skf_objects())) | |||
| seasons = {} | |||
| for time, almanac_season in zip(times, almanac_seasons): | |||
| if almanac_season == 0: | |||
| season = 'MARCH' | |||
| elif almanac_season == 1: | |||
| season = 'JUNE' | |||
| elif almanac_season == 2: | |||
| season = 'SEPTEMBER' | |||
| elif almanac_season == 3: | |||
| season = 'DECEMBER' | |||
| else: | |||
| raise AssertionError | |||
| seasons[season] = time.utc_iso() | |||
| return seasons | |||
| ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster)) | |||
| return ephemerides | |||
| @@ -8,7 +8,7 @@ msgid "" | |||
| msgstr "" | |||
| "Project-Id-Version: kosmorro 0.7.0\n" | |||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | |||
| "POT-Creation-Date: 2020-04-16 17:57+0200\n" | |||
| "POT-Creation-Date: 2020-04-18 15:51+0200\n" | |||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |||
| "Language-Team: LANGUAGE <LL@li.org>\n" | |||
| @@ -79,43 +79,43 @@ msgstr "" | |||
| msgid "%s's largest elongation" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:233 | |||
| #: kosmorrolib/data.py:261 | |||
| msgid "Sun" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:234 | |||
| #: kosmorrolib/data.py:262 | |||
| msgid "Moon" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:235 | |||
| #: kosmorrolib/data.py:263 | |||
| msgid "Mercury" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:236 | |||
| #: kosmorrolib/data.py:264 | |||
| msgid "Venus" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:237 | |||
| #: kosmorrolib/data.py:265 | |||
| msgid "Mars" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:238 | |||
| #: kosmorrolib/data.py:266 | |||
| msgid "Jupiter" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:239 | |||
| #: kosmorrolib/data.py:267 | |||
| msgid "Saturn" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:240 | |||
| #: kosmorrolib/data.py:268 | |||
| msgid "Uranus" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:241 | |||
| #: kosmorrolib/data.py:269 | |||
| msgid "Neptune" | |||
| msgstr "" | |||
| #: kosmorrolib/data.py:242 | |||
| #: kosmorrolib/data.py:270 | |||
| msgid "Pluto" | |||
| msgstr "" | |||
| @@ -131,68 +131,68 @@ msgstr "" | |||
| msgid "{hours}:{minutes}" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:148 | |||
| #: kosmorrolib/dumper.py:150 | |||
| msgid "Expected events:" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:152 | |||
| #: kosmorrolib/dumper.py:154 | |||
| msgid "Note: All the hours are given in UTC." | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:157 | |||
| #: kosmorrolib/dumper.py:159 | |||
| msgid "Note: All the hours are given in the UTC{offset} timezone." | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:203 kosmorrolib/dumper.py:272 | |||
| #: kosmorrolib/dumper.py:205 kosmorrolib/dumper.py:274 | |||
| msgid "Object" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:204 kosmorrolib/dumper.py:273 | |||
| #: kosmorrolib/dumper.py:206 kosmorrolib/dumper.py:275 | |||
| msgid "Rise time" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:205 kosmorrolib/dumper.py:274 | |||
| #: kosmorrolib/dumper.py:207 kosmorrolib/dumper.py:276 | |||
| msgid "Culmination time" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:206 kosmorrolib/dumper.py:275 | |||
| #: kosmorrolib/dumper.py:208 kosmorrolib/dumper.py:277 | |||
| msgid "Set time" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:220 kosmorrolib/dumper.py:278 | |||
| #: kosmorrolib/dumper.py:222 kosmorrolib/dumper.py:280 | |||
| msgid "Moon phase:" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:221 | |||
| #: kosmorrolib/dumper.py:223 | |||
| msgid "{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:259 | |||
| #: kosmorrolib/dumper.py:261 | |||
| msgid "A Summary of your Sky" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:263 | |||
| #: kosmorrolib/dumper.py:265 | |||
| 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:269 | |||
| #: kosmorrolib/dumper.py:271 | |||
| msgid "" | |||
| "Don't forget to check the weather forecast before you go out with your " | |||
| "equipment." | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:271 | |||
| #: kosmorrolib/dumper.py:273 | |||
| msgid "Ephemerides of the day" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:280 | |||
| #: kosmorrolib/dumper.py:282 | |||
| msgid "Expected events" | |||
| msgstr "" | |||
| #: kosmorrolib/dumper.py:355 | |||
| #: kosmorrolib/dumper.py:354 | |||
| msgid "" | |||
| "Building PDFs was not possible, because some dependencies are not " | |||
| "installed.\n" | |||
| @@ -200,93 +200,93 @@ msgid "" | |||
| "information." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:58 | |||
| #: kosmorrolib/main.py:61 | |||
| msgid "" | |||
| "Save the planet and paper!\n" | |||
| "Consider printing you PDF document only if really necessary, and use the " | |||
| "other side of the sheet." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:62 | |||
| #: kosmorrolib/main.py:65 | |||
| msgid "" | |||
| "PDF output will not contain the ephemerides, because you didn't provide " | |||
| "the observation coordinate." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:91 | |||
| #: kosmorrolib/main.py:93 | |||
| msgid "Could not save the output in \"{path}\": {error}" | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:96 | |||
| #: kosmorrolib/main.py:98 | |||
| msgid "Selected output format needs an output file (--output)." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:115 | |||
| #: kosmorrolib/main.py:117 | |||
| msgid "Running on Python {python_version}" | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:121 | |||
| #: kosmorrolib/main.py:123 | |||
| msgid "Do you really want to clear Kosmorro's cache? [yN] " | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:128 | |||
| #: kosmorrolib/main.py:130 | |||
| msgid "Answer did not match expected options, cache not cleared." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:137 | |||
| #: kosmorrolib/main.py:139 | |||
| msgid "" | |||
| "Compute the ephemerides and the events for a given date, at a given " | |||
| "position on Earth." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:139 | |||
| #: kosmorrolib/main.py:141 | |||
| 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:144 | |||
| #: kosmorrolib/main.py:146 | |||
| msgid "Show the program version" | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:146 | |||
| #: kosmorrolib/main.py:148 | |||
| msgid "Delete all the files Kosmorro stored in the cache." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:148 | |||
| #: kosmorrolib/main.py:150 | |||
| msgid "The format under which the information have to be output" | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:150 | |||
| #: kosmorrolib/main.py:152 | |||
| msgid "" | |||
| "The observer's latitude on Earth. Can also be set in the " | |||
| "KOSMORRO_LATITUDE environment variable." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:153 | |||
| #: kosmorrolib/main.py:155 | |||
| msgid "" | |||
| "The observer's longitude on Earth. Can also be set in the " | |||
| "KOSMORRO_LONGITUDE environment variable." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:156 | |||
| #: kosmorrolib/main.py:158 | |||
| msgid "" | |||
| "The date for which the ephemerides must be computed (in the YYYY-MM-DD " | |||
| "format). Defaults to the current date ({default_date})" | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:160 | |||
| #: kosmorrolib/main.py:162 | |||
| 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:163 | |||
| #: kosmorrolib/main.py:165 | |||
| msgid "Disable the colors in the console." | |||
| msgstr "" | |||
| #: kosmorrolib/main.py:165 | |||
| #: kosmorrolib/main.py:167 | |||
| msgid "" | |||
| "A file to export the output to. If not given, the standard output is " | |||
| "used. This argument is needed for PDF format." | |||
| @@ -24,19 +24,22 @@ import sys | |||
| from datetime import date | |||
| from termcolor import colored | |||
| from kosmorrolib.version import VERSION | |||
| from kosmorrolib import dumper | |||
| from kosmorrolib import core | |||
| from kosmorrolib import events | |||
| from kosmorrolib.i18n import _ | |||
| from .ephemerides import EphemeridesComputer, Position | |||
| from . import dumper | |||
| from . import core | |||
| from . import events | |||
| from .data import Position, EARTH | |||
| from .exceptions import UnavailableFeatureError | |||
| from .i18n import _ | |||
| from . import ephemerides | |||
| from .version import VERSION | |||
| def main(): | |||
| environment = core.get_env() | |||
| output_formats = get_dumpers() | |||
| args = get_args(list(output_formats.keys())) | |||
| output_format = args.format | |||
| if args.special_action is not None: | |||
| return 0 if args.special_action() else 1 | |||
| @@ -50,11 +53,11 @@ def main(): | |||
| position = None | |||
| if args.latitude is not None or args.longitude is not None: | |||
| position = Position(args.latitude, args.longitude) | |||
| position = Position(args.latitude, args.longitude, EARTH) | |||
| elif environment.latitude is not None and environment.longitude is not None: | |||
| position = Position(float(environment.latitude), float(environment.longitude)) | |||
| position = Position(float(environment.latitude), float(environment.longitude), EARTH) | |||
| if args.format == 'pdf': | |||
| if output_format == 'pdf': | |||
| print(_('Save the planet and paper!\n' | |||
| 'Consider printing you PDF document only if really necessary, and use the other side of the sheet.')) | |||
| if position is None: | |||
| @@ -63,8 +66,8 @@ def main(): | |||
| "coordinate."), 'yellow')) | |||
| try: | |||
| ephemeris = EphemeridesComputer(position) | |||
| ephemerides = ephemeris.compute_ephemerides(compute_date) | |||
| eph = ephemerides.get_ephemerides(date=compute_date, position=position) if position is not None else None | |||
| moon_phase = ephemerides.get_moon_phase(compute_date) | |||
| events_list = events.search_events(compute_date) | |||
| @@ -75,10 +78,9 @@ def main(): | |||
| elif timezone is None: | |||
| timezone = 0 | |||
| selected_dumper = output_formats[args.format](ephemerides, events_list, | |||
| date=compute_date, timezone=timezone, | |||
| with_colors=args.colors) | |||
| output = selected_dumper.to_string() | |||
| format_dumper = output_formats[output_format](ephemerides=eph, moon_phase=moon_phase, events=events_list, | |||
| date=compute_date, timezone=timezone, with_colors=args.colors) | |||
| output = format_dumper.to_string() | |||
| except UnavailableFeatureError as error: | |||
| print(colored(error.msg, 'red')) | |||
| return 2 | |||
| @@ -90,7 +92,7 @@ def main(): | |||
| except OSError as error: | |||
| print(_('Could not save the output in "{path}": {error}').format(path=args.output, | |||
| error=error.strerror)) | |||
| elif not selected_dumper.is_file_output_needed(): | |||
| elif not format_dumper.is_file_output_needed(): | |||
| print(output) | |||
| else: | |||
| print(colored(_('Selected output format needs an output file (--output).'), color='red')) | |||
| @@ -3,3 +3,4 @@ from .data import * | |||
| from .dumper import * | |||
| from .ephemerides import * | |||
| from .events import * | |||
| from .testutils import * | |||
| @@ -11,86 +11,110 @@ class DumperTestCase(unittest.TestCase): | |||
| def test_json_dumper_returns_correct_json(self): | |||
| self.assertEqual('{\n' | |||
| ' "ephemerides": [\n' | |||
| ' {\n' | |||
| ' "object": {\n' | |||
| ' "name": "Mars",\n' | |||
| ' "type": "planet",\n' | |||
| ' "radius": null\n' | |||
| ' },\n' | |||
| ' "rise_time": null,\n' | |||
| ' "culmination_time": null,\n' | |||
| ' "set_time": null\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "moon_phase": {\n' | |||
| ' "next_phase_date": "2019-10-21T00:00:00",\n' | |||
| ' "phase": "FULL_MOON",\n' | |||
| ' "date": "2019-10-14T00:00:00"\n' | |||
| ' "time": "2019-10-14T00:00:00",\n' | |||
| ' "next": {\n' | |||
| ' "phase": "LAST_QUARTER",\n' | |||
| ' "time": "2019-10-21T00:00:00"\n' | |||
| ' }\n' | |||
| ' },\n' | |||
| ' "events": [\n' | |||
| ' {\n' | |||
| ' "event_type": "OPPOSITION",\n' | |||
| ' "objects": [\n' | |||
| ' "Mars"\n' | |||
| ' {\n' | |||
| ' "name": "Mars",\n' | |||
| ' "type": "planet",\n' | |||
| ' "radius": null\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "start_time": "2019-10-14T23:00:00",\n' | |||
| ' "end_time": null,\n' | |||
| ' "event": "OPPOSITION",\n' | |||
| ' "starts_at": "2019-10-14T23:00:00",\n' | |||
| ' "ends_at": null,\n' | |||
| ' "details": null\n' | |||
| ' },\n' | |||
| ' {\n' | |||
| ' "event_type": "MAXIMAL_ELONGATION",\n' | |||
| ' "objects": [\n' | |||
| ' "Venus"\n' | |||
| ' {\n' | |||
| ' "name": "Venus",\n' | |||
| ' "type": "planet",\n' | |||
| ' "radius": null\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "start_time": "2019-10-14T12:00:00",\n' | |||
| ' "end_time": null,\n' | |||
| ' "event": "MAXIMAL_ELONGATION",\n' | |||
| ' "starts_at": "2019-10-14T12:00:00",\n' | |||
| ' "ends_at": null,\n' | |||
| ' "details": "42.0\\u00b0"\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "ephemerides": [\n' | |||
| ' {\n' | |||
| ' "object": "Mars",\n' | |||
| ' "details": {\n' | |||
| ' "rise_time": null,\n' | |||
| ' "culmination_time": null,\n' | |||
| ' "set_time": null\n' | |||
| ' }\n' | |||
| ' }\n' | |||
| ' ]\n' | |||
| '}', JsonDumper(self._get_data(), self._get_events()).to_string()) | |||
| '}', JsonDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events()).to_string()) | |||
| data = self._get_data(aster_rise_set=True) | |||
| self.assertEqual('{\n' | |||
| ' "ephemerides": [\n' | |||
| ' {\n' | |||
| ' "object": {\n' | |||
| ' "name": "Mars",\n' | |||
| ' "type": "planet",\n' | |||
| ' "radius": null\n' | |||
| ' },\n' | |||
| ' "rise_time": "2019-10-14T08:00:00",\n' | |||
| ' "culmination_time": "2019-10-14T13:00:00",\n' | |||
| ' "set_time": "2019-10-14T23:00:00"\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "moon_phase": {\n' | |||
| ' "next_phase_date": "2019-10-21T00:00:00",\n' | |||
| ' "phase": "FULL_MOON",\n' | |||
| ' "date": "2019-10-14T00:00:00"\n' | |||
| ' "time": "2019-10-14T00:00:00",\n' | |||
| ' "next": {\n' | |||
| ' "phase": "LAST_QUARTER",\n' | |||
| ' "time": "2019-10-21T00:00:00"\n' | |||
| ' }\n' | |||
| ' },\n' | |||
| ' "events": [\n' | |||
| ' {\n' | |||
| ' "event_type": "OPPOSITION",\n' | |||
| ' "objects": [\n' | |||
| ' "Mars"\n' | |||
| ' {\n' | |||
| ' "name": "Mars",\n' | |||
| ' "type": "planet",\n' | |||
| ' "radius": null\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "start_time": "2019-10-14T23:00:00",\n' | |||
| ' "end_time": null,\n' | |||
| ' "event": "OPPOSITION",\n' | |||
| ' "starts_at": "2019-10-14T23:00:00",\n' | |||
| ' "ends_at": null,\n' | |||
| ' "details": null\n' | |||
| ' },\n' | |||
| ' {\n' | |||
| ' "event_type": "MAXIMAL_ELONGATION",\n' | |||
| ' "objects": [\n' | |||
| ' "Venus"\n' | |||
| ' {\n' | |||
| ' "name": "Venus",\n' | |||
| ' "type": "planet",\n' | |||
| ' "radius": null\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "start_time": "2019-10-14T12:00:00",\n' | |||
| ' "end_time": null,\n' | |||
| ' "event": "MAXIMAL_ELONGATION",\n' | |||
| ' "starts_at": "2019-10-14T12:00:00",\n' | |||
| ' "ends_at": null,\n' | |||
| ' "details": "42.0\\u00b0"\n' | |||
| ' }\n' | |||
| ' ],\n' | |||
| ' "ephemerides": [\n' | |||
| ' {\n' | |||
| ' "object": "Mars",\n' | |||
| ' "details": {\n' | |||
| ' "rise_time": "2019-10-14T08:00:00",\n' | |||
| ' "culmination_time": "2019-10-14T13:00:00",\n' | |||
| ' "set_time": "2019-10-14T23:00:00"\n' | |||
| ' }\n' | |||
| ' }\n' | |||
| ' ]\n' | |||
| '}', JsonDumper(data, | |||
| self._get_events() | |||
| ).to_string()) | |||
| '}', JsonDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), | |||
| self._get_events()).to_string()) | |||
| def test_text_dumper_without_events(self): | |||
| ephemerides = self._get_data() | |||
| ephemerides = self._get_ephemerides() | |||
| self.assertEqual('Monday October 14, 2019\n\n' | |||
| 'Object Rise time Culmination time Set time\n' | |||
| '-------- ----------- ------------------ ----------\n' | |||
| @@ -98,9 +122,9 @@ class DumperTestCase(unittest.TestCase): | |||
| 'Moon phase: Full Moon\n' | |||
| 'Last Quarter on Monday October 21, 2019 at 00:00\n\n' | |||
| 'Note: All the hours are given in UTC.', | |||
| TextDumper(ephemerides, [], date=date(2019, 10, 14), with_colors=False).to_string()) | |||
| TextDumper(ephemerides, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string()) | |||
| ephemerides = self._get_data(aster_rise_set=True) | |||
| ephemerides = self._get_ephemerides(aster_rise_set=True) | |||
| self.assertEqual('Monday October 14, 2019\n\n' | |||
| 'Object Rise time Culmination time Set time\n' | |||
| '-------- ----------- ------------------ ----------\n' | |||
| @@ -108,10 +132,10 @@ class DumperTestCase(unittest.TestCase): | |||
| 'Moon phase: Full Moon\n' | |||
| 'Last Quarter on Monday October 21, 2019 at 00:00\n\n' | |||
| 'Note: All the hours are given in UTC.', | |||
| TextDumper(ephemerides, [], date=date(2019, 10, 14), with_colors=False).to_string()) | |||
| TextDumper(ephemerides, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string()) | |||
| def test_text_dumper_with_events(self): | |||
| ephemerides = self._get_data() | |||
| ephemerides = self._get_ephemerides() | |||
| self.assertEqual("Monday October 14, 2019\n\n" | |||
| "Object Rise time Culmination time Set time\n" | |||
| "-------- ----------- ------------------ ----------\n" | |||
| @@ -122,10 +146,9 @@ class DumperTestCase(unittest.TestCase): | |||
| "23:00 Mars is in opposition\n" | |||
| "12:00 Venus's largest elongation (42.0°)\n\n" | |||
| "Note: All the hours are given in UTC.", | |||
| TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) | |||
| TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) | |||
| def test_text_dumper_without_ephemerides_and_with_events(self): | |||
| ephemerides = self._get_data(False) | |||
| self.assertEqual('Monday October 14, 2019\n\n' | |||
| 'Moon phase: Full Moon\n' | |||
| 'Last Quarter on Monday October 21, 2019 at 00:00\n\n' | |||
| @@ -133,10 +156,12 @@ class DumperTestCase(unittest.TestCase): | |||
| '23:00 Mars is in opposition\n' | |||
| "12:00 Venus's largest elongation (42.0°)\n\n" | |||
| 'Note: All the hours are given in UTC.', | |||
| TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) | |||
| TextDumper(None, self._get_moon_phase(), self._get_events(), | |||
| date=date(2019, 10, 14), with_colors=False).to_string()) | |||
| def test_timezone_is_taken_in_account(self): | |||
| ephemerides = self._get_data(aster_rise_set=True) | |||
| ephemerides = self._get_ephemerides(aster_rise_set=True) | |||
| self.assertEqual('Monday October 14, 2019\n\n' | |||
| 'Object Rise time Culmination time Set time\n' | |||
| '-------- ----------- ------------------ -------------\n' | |||
| @@ -147,9 +172,11 @@ class DumperTestCase(unittest.TestCase): | |||
| 'Oct 15, 00:00 Mars is in opposition\n' | |||
| "13:00 Venus's largest elongation (42.0°)\n\n" | |||
| 'Note: All the hours are given in the UTC+1 timezone.', | |||
| TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False, timezone=1).to_string()) | |||
| TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), | |||
| with_colors=False, timezone=1).to_string()) | |||
| ephemerides = self._get_ephemerides(aster_rise_set=True) | |||
| ephemerides = self._get_data(aster_rise_set=True) | |||
| self.assertEqual('Monday October 14, 2019\n\n' | |||
| 'Object Rise time Culmination time Set time\n' | |||
| '-------- ----------- ------------------ ----------\n' | |||
| @@ -160,10 +187,13 @@ class DumperTestCase(unittest.TestCase): | |||
| '22:00 Mars is in opposition\n' | |||
| "11:00 Venus's largest elongation (42.0°)\n\n" | |||
| 'Note: All the hours are given in the UTC-1 timezone.', | |||
| TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False, timezone=-1).to_string()) | |||
| TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), | |||
| with_colors=False, timezone=-1).to_string()) | |||
| def test_latex_dumper(self): | |||
| latex = _LatexDumper(self._get_data(), self._get_events(), date=date(2019, 10, 14)).to_string() | |||
| latex = _LatexDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events(), | |||
| date=date(2019, 10, 14)).to_string() | |||
| self.assertRegex(latex, 'Monday October 14, 2019') | |||
| self.assertRegex(latex, 'Full Moon') | |||
| self.assertRegex(latex, r'\\section{\\sffamily Expected events}') | |||
| @@ -172,12 +202,14 @@ class DumperTestCase(unittest.TestCase): | |||
| self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}') | |||
| self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}") | |||
| latex = _LatexDumper(self._get_data(aster_rise_set=True), | |||
| latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), | |||
| self._get_events(), date=date(2019, 10, 14)).to_string() | |||
| self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') | |||
| def test_latex_dumper_without_ephemerides(self): | |||
| latex = _LatexDumper(self._get_data(False), self._get_events(), date=date(2019, 10, 14)).to_string() | |||
| latex = _LatexDumper(None, self._get_moon_phase(), self._get_events(), | |||
| date=date(2019, 10, 14)).to_string() | |||
| self.assertRegex(latex, 'Monday October 14, 2019') | |||
| self.assertRegex(latex, 'Full Moon') | |||
| self.assertRegex(latex, r'\\section{\\sffamily Expected events}') | |||
| @@ -188,7 +220,8 @@ class DumperTestCase(unittest.TestCase): | |||
| self.assertNotRegex(latex, r'\\section{\\sffamily Ephemerides of the day}') | |||
| def test_latex_dumper_without_events(self): | |||
| latex = _LatexDumper(self._get_data(), [], date=date(2019, 10, 14)).to_string() | |||
| latex = _LatexDumper(self._get_ephemerides(), self._get_moon_phase(), [], date=date(2019, 10, 14)).to_string() | |||
| self.assertRegex(latex, 'Monday October 14, 2019') | |||
| self.assertRegex(latex, 'Full Moon') | |||
| self.assertRegex(latex, r'\\object\{Mars\}\{-\}\{-\}\{-\}') | |||
| @@ -197,16 +230,16 @@ class DumperTestCase(unittest.TestCase): | |||
| self.assertNotRegex(latex, r'\\section{\\sffamily Expected events}') | |||
| @staticmethod | |||
| def _get_data(has_ephemerides: bool = True, aster_rise_set=False): | |||
| def _get_ephemerides(aster_rise_set=False) -> [AsterEphemerides]: | |||
| rise_time = datetime(2019, 10, 14, 8) if aster_rise_set else None | |||
| culmination_time = datetime(2019, 10, 14, 13) if aster_rise_set else None | |||
| set_time = datetime(2019, 10, 14, 23) if aster_rise_set else None | |||
| return { | |||
| 'moon_phase': MoonPhase('FULL_MOON', datetime(2019, 10, 14), datetime(2019, 10, 21)), | |||
| 'details': [Planet('Mars', 'MARS', | |||
| AsterEphemerides(rise_time, culmination_time, set_time))] if has_ephemerides else [] | |||
| } | |||
| return [AsterEphemerides(rise_time, culmination_time, set_time, Planet('Mars', 'MARS'))] | |||
| @staticmethod | |||
| def _get_moon_phase(): | |||
| return MoonPhase('FULL_MOON', datetime(2019, 10, 14), datetime(2019, 10, 21)) | |||
| @staticmethod | |||
| def _get_events(): | |||
| @@ -1,106 +1,111 @@ | |||
| import unittest | |||
| from kosmorrolib.ephemerides import EphemeridesComputer | |||
| from kosmorrolib.core import get_skf_objects | |||
| from kosmorrolib.data import Star, Position, MoonPhase | |||
| from .testutils import expect_assertions | |||
| from kosmorrolib import ephemerides | |||
| from kosmorrolib.data import EARTH, Position, MoonPhase | |||
| from datetime import date | |||
| class EphemeridesComputerTestCase(unittest.TestCase): | |||
| class EphemeridesTestCase(unittest.TestCase): | |||
| def test_get_ephemerides_for_aster_returns_correct_hours(self): | |||
| position = Position(0, 0) | |||
| position.observation_planet = get_skf_objects()['earth'] | |||
| star = EphemeridesComputer.get_asters_ephemerides_for_aster(Star('Sun', skyfield_name='sun'), | |||
| date=date(2019, 11, 18), | |||
| position=position) | |||
| position = Position(0, 0, EARTH) | |||
| eph = ephemerides.get_ephemerides(date=date(2019, 11, 18), | |||
| position=position) | |||
| self.assertRegex(star.ephemerides.rise_time.isoformat(), '^2019-11-18T05:41:') | |||
| self.assertRegex(star.ephemerides.culmination_time.isoformat(), '^2019-11-18T11:45:') | |||
| self.assertRegex(star.ephemerides.set_time.isoformat(), '^2019-11-18T17:48:') | |||
| @expect_assertions(self.assertRegex, num=3) | |||
| def do_assertions(assert_regex): | |||
| for ephemeris in eph: | |||
| if ephemeris.object.skyfield_name == 'SUN': | |||
| assert_regex(ephemeris.rise_time.isoformat(), '^2019-11-18T05:41:') | |||
| assert_regex(ephemeris.culmination_time.isoformat(), '^2019-11-18T11:45:') | |||
| assert_regex(ephemeris.set_time.isoformat(), '^2019-11-18T17:48:') | |||
| break | |||
| do_assertions() | |||
| ################################################################################################################### | |||
| ### MOON PHASE TESTS ### | |||
| ################################################################################################################### | |||
| def test_moon_phase_new_moon(self): | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 25)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 25)) | |||
| self.assertEqual('WANING_CRESCENT', phase.identifier) | |||
| self.assertIsNone(phase.time) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 26)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 26)) | |||
| self.assertEqual('NEW_MOON', phase.identifier) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 27)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 27)) | |||
| self.assertEqual('WAXING_CRESCENT', phase.identifier) | |||
| self.assertIsNone(phase.time) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') | |||
| def test_moon_phase_first_crescent(self): | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 3)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 3)) | |||
| self.assertEqual('WAXING_CRESCENT', phase.identifier) | |||
| self.assertIsNone(phase.time) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-04T') | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 4)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 4)) | |||
| self.assertEqual('FIRST_QUARTER', phase.identifier) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 5)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 5)) | |||
| self.assertEqual('WAXING_GIBBOUS', phase.identifier) | |||
| self.assertIsNone(phase.time) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
| def test_moon_phase_full_moon(self): | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 11)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 11)) | |||
| self.assertEqual('WAXING_GIBBOUS', phase.identifier) | |||
| self.assertIsNone(phase.time) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 12)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 12)) | |||
| self.assertEqual('FULL_MOON', phase.identifier) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 13)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 13)) | |||
| self.assertEqual('WANING_GIBBOUS', phase.identifier) | |||
| self.assertIsNone(phase.time) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
| def test_moon_phase_last_quarter(self): | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 18)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 18)) | |||
| self.assertEqual('WANING_GIBBOUS', phase.identifier) | |||
| self.assertIsNone(phase.time) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 19)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 19)) | |||
| self.assertEqual('LAST_QUARTER', phase.identifier) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
| phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 20)) | |||
| phase = ephemerides.get_moon_phase(date(2019, 11, 20)) | |||
| self.assertEqual('WANING_CRESCENT', phase.identifier) | |||
| self.assertIsNone(phase.time) | |||
| self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
| def test_moon_phase_prediction(self): | |||
| phase = MoonPhase('NEW_MOON', None, None) | |||
| self.assertEqual('First Quarter', phase.get_next_phase()) | |||
| self.assertEqual('First Quarter', phase.get_next_phase_name()) | |||
| phase = MoonPhase('WAXING_CRESCENT', None, None) | |||
| self.assertEqual('First Quarter', phase.get_next_phase()) | |||
| self.assertEqual('First Quarter', phase.get_next_phase_name()) | |||
| phase = MoonPhase('FIRST_QUARTER', None, None) | |||
| self.assertEqual('Full Moon', phase.get_next_phase()) | |||
| self.assertEqual('Full Moon', phase.get_next_phase_name()) | |||
| phase = MoonPhase('WAXING_GIBBOUS', None, None) | |||
| self.assertEqual('Full Moon', phase.get_next_phase()) | |||
| self.assertEqual('Full Moon', phase.get_next_phase_name()) | |||
| phase = MoonPhase('FULL_MOON', None, None) | |||
| self.assertEqual('Last Quarter', phase.get_next_phase()) | |||
| self.assertEqual('Last Quarter', phase.get_next_phase_name()) | |||
| phase = MoonPhase('WANING_GIBBOUS', None, None) | |||
| self.assertEqual('Last Quarter', phase.get_next_phase()) | |||
| self.assertEqual('Last Quarter', phase.get_next_phase_name()) | |||
| phase = MoonPhase('LAST_QUARTER', None, None) | |||
| self.assertEqual('New Moon', phase.get_next_phase()) | |||
| self.assertEqual('New Moon', phase.get_next_phase_name()) | |||
| phase = MoonPhase('WANING_CRESCENT', None, None) | |||
| self.assertEqual('New Moon', phase.get_next_phase()) | |||
| self.assertEqual('New Moon', phase.get_next_phase_name()) | |||
| if __name__ == '__main__': | |||
| @@ -0,0 +1,48 @@ | |||
| import functools | |||
| from unittest import mock | |||
| def expect_assertions(assert_fun, num=1): | |||
| """Asserts that an assertion function is called as expected. | |||
| This is very useful when the assertions are in loops. | |||
| To use it, create a nested function in the the test function. | |||
| The nested function will receive as parameter the mocked assertion function to use in place of the original one. | |||
| Finally, run the nested function. | |||
| Example of use: | |||
| >>> # the function we test: | |||
| >>> def my_sum_function(n, m): | |||
| >>> # some code here | |||
| >>> pass | |||
| >>> # The unit test: | |||
| >>> def test_sum(self): | |||
| >>> @expect_assertions(self.assertEqual, num=10): | |||
| >>> def make_assertions(assert_equal): | |||
| >>> for i in range (-5, 5): | |||
| >>> for j in range(-5, 5): | |||
| >>> assert_equal(i + j, my_sum_function(i, j) | |||
| >>> | |||
| >>> make_assertions() # You don't need to give any parameter, the decorator does it for you. | |||
| :param assert_fun: the assertion function to test | |||
| :param num: the number of times the assertion function is expected to be called | |||
| """ | |||
| assert_fun_mock = mock.Mock(side_effect=assert_fun) | |||
| def fun_decorator(fun): | |||
| @functools.wraps(fun) | |||
| def sniff_function(): | |||
| fun(assert_fun_mock) | |||
| count = assert_fun_mock.call_count | |||
| if count != num: | |||
| raise AssertionError('Expected %d call(s) to function %s but called %d time(s).' % (num, | |||
| assert_fun.__name__, | |||
| count)) | |||
| return sniff_function | |||
| return fun_decorator | |||