Browse Source

refactor: simplify ephemerides, remove dead code

BREAKING CHANGE: the JSON format has deeply changed to enhance its
consistency
tags/v0.8.0
Jérôme Deuchnord 4 years ago
parent
commit
6618712030
No known key found for this signature in database GPG Key ID: BC6F3C345B7D33B0
9 changed files with 434 additions and 345 deletions
  1. +80
    -34
      kosmorrolib/data.py
  2. +68
    -69
      kosmorrolib/dumper.py
  3. +38
    -83
      kosmorrolib/ephemerides.py
  4. +44
    -44
      kosmorrolib/locales/messages.pot
  5. +18
    -16
      kosmorrolib/main.py
  6. +1
    -0
      test/__init__.py
  7. +100
    -67
      test/dumper.py
  8. +37
    -32
      test/ephemerides.py
  9. +48
    -0
      test/testutils.py

+ 80
- 34
kosmorrolib/data.py View File

@@ -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

+ 68
- 69
kosmorrolib/dumper.py View File

@@ -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:


+ 38
- 83
kosmorrolib/ephemerides.py View File

@@ -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

+ 44
- 44
kosmorrolib/locales/messages.pot View File

@@ -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."


+ 18
- 16
kosmorrolib/main.py View File

@@ -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'))


+ 1
- 0
test/__init__.py View File

@@ -3,3 +3,4 @@ from .data import *
from .dumper import *
from .ephemerides import *
from .events import *
from .testutils import *

+ 100
- 67
test/dumper.py View File

@@ -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():


+ 37
- 32
test/ephemerides.py View File

@@ -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__':


+ 48
- 0
test/testutils.py View File

@@ -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

Loading…
Cancel
Save