BREAKING CHANGE: the JSON format has deeply changed to enhance its consistencytags/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 |