@@ -1,3 +1,6 @@ | |||
black: | |||
pipenv run black kosmorrolib tests | |||
.PHONY: test | |||
tests: legacy-tests | |||
python3 tests.py | |||
@@ -1,9 +1,9 @@ | |||
__title__ = 'kosmorrolib' | |||
__description__ = 'A library that computes your ephemerides' | |||
__url__ = 'http://kosmorro.space' | |||
__version__ = '0.9.0' | |||
__title__ = "kosmorrolib" | |||
__description__ = "A library that computes your ephemerides" | |||
__url__ = "http://kosmorro.space" | |||
__version__ = "0.9.0" | |||
__build__ = 0x000900 | |||
__author__ = 'Jérôme Deuchnord' | |||
__author_email__ = 'jerome@deuchnord.fr' | |||
__license__ = 'AGPL' | |||
__copyright__ = 'Copyright 2021 Jérôme Deuchnord' | |||
__author__ = "Jérôme Deuchnord" | |||
__author_email__ = "jerome@deuchnord.fr" | |||
__license__ = "AGPL" | |||
__copyright__ = "Copyright 2021 Jérôme Deuchnord" |
@@ -28,7 +28,8 @@ from skyfield.api import Loader | |||
from skyfield.timelib import Time | |||
from skyfield.nutationlib import iau2000b | |||
CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache' | |||
CACHE_FOLDER = str(Path.home()) + "/.kosmorro-cache" | |||
class Environment: | |||
def __init__(self): | |||
@@ -46,18 +47,20 @@ class Environment: | |||
def __len__(self): | |||
return len(self._vars) | |||
def get_env() -> Environment: | |||
environment = Environment() | |||
for var in os.environ: | |||
if not re.search('^KOSMORRO_', var): | |||
if not re.search("^KOSMORRO_", var): | |||
continue | |||
[_, env] = var.split('_', 1) | |||
[_, env] = var.split("_", 1) | |||
environment.__set__(env.lower(), os.getenv(var)) | |||
return environment | |||
def get_loader(): | |||
return Loader(CACHE_FOLDER) | |||
@@ -67,7 +70,7 @@ def get_timescale(): | |||
def get_skf_objects(): | |||
return get_loader()('de421.bsp') | |||
return get_loader()("de421.bsp") | |||
def get_iau2000b(time: Time): | |||
@@ -92,26 +95,32 @@ def flatten_list(the_list: list): | |||
def get_date(date_arg: str) -> date: | |||
if re.match(r'^\d{4}-\d{2}-\d{2}$', date_arg): | |||
if re.match(r"^\d{4}-\d{2}-\d{2}$", date_arg): | |||
try: | |||
return date.fromisoformat(date_arg) | |||
except ValueError as error: | |||
raise ValueError(_('The date {date} is not valid: {error}').format(date=date_arg, | |||
error=error.args[0])) from error | |||
elif re.match(r'^([+-])(([0-9]+)y)?[ ]?(([0-9]+)m)?[ ]?(([0-9]+)d)?$', date_arg): | |||
raise ValueError( | |||
_("The date {date} is not valid: {error}").format( | |||
date=date_arg, error=error.args[0] | |||
) | |||
) from error | |||
elif re.match(r"^([+-])(([0-9]+)y)?[ ]?(([0-9]+)m)?[ ]?(([0-9]+)d)?$", date_arg): | |||
def get_offset(date_arg: str, signifier: str): | |||
if re.search(r'([0-9]+)' + signifier, date_arg): | |||
return abs(int(re.search(r'[+-]?([0-9]+)' + signifier, date_arg).group(0)[:-1])) | |||
if re.search(r"([0-9]+)" + signifier, date_arg): | |||
return abs( | |||
int(re.search(r"[+-]?([0-9]+)" + signifier, date_arg).group(0)[:-1]) | |||
) | |||
return 0 | |||
days = get_offset(date_arg, 'd') | |||
months = get_offset(date_arg, 'm') | |||
years = get_offset(date_arg, 'y') | |||
days = get_offset(date_arg, "d") | |||
months = get_offset(date_arg, "m") | |||
years = get_offset(date_arg, "y") | |||
if date_arg[0] == '+': | |||
if date_arg[0] == "+": | |||
return date.today() + relativedelta(days=days, months=months, years=years) | |||
return date.today() - relativedelta(days=days, months=months, years=years) | |||
else: | |||
error_msg = 'The date {date} does not match the required YYYY-MM-DD format or the offset format.' | |||
error_msg = "The date {date} does not match the required YYYY-MM-DD format or the offset format." | |||
raise ValueError(error_msg.format(date=date_arg)) |
@@ -36,7 +36,12 @@ class Serializable(ABC): | |||
class MoonPhase(Serializable): | |||
def __init__(self, phase_type: MoonPhaseType, time: datetime = None, next_phase_date: datetime = None): | |||
def __init__( | |||
self, | |||
phase_type: MoonPhaseType, | |||
time: datetime = None, | |||
next_phase_date: datetime = None, | |||
): | |||
self.phase_type = phase_type | |||
self.time = time | |||
self.next_phase_date = next_phase_date | |||
@@ -44,7 +49,10 @@ class MoonPhase(Serializable): | |||
def get_next_phase(self): | |||
if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]: | |||
return MoonPhaseType.FIRST_QUARTER | |||
if self.phase_type in [MoonPhaseType.FIRST_QUARTER, MoonPhaseType.WAXING_GIBBOUS]: | |||
if self.phase_type in [ | |||
MoonPhaseType.FIRST_QUARTER, | |||
MoonPhaseType.WAXING_GIBBOUS, | |||
]: | |||
return MoonPhaseType.FULL_MOON | |||
if self.phase_type in [MoonPhaseType.FULL_MOON, MoonPhaseType.WANING_GIBBOUS]: | |||
return MoonPhaseType.LAST_QUARTER | |||
@@ -53,12 +61,12 @@ class MoonPhase(Serializable): | |||
def serialize(self) -> dict: | |||
return { | |||
'phase': self.phase_type.name, | |||
'time': self.time.isoformat() if self.time is not None else None, | |||
'next': { | |||
'phase': self.get_next_phase().name, | |||
'time': self.next_phase_date.isoformat() | |||
} | |||
"phase": self.phase_type.name, | |||
"time": self.time.isoformat() if self.time is not None else None, | |||
"next": { | |||
"phase": self.get_next_phase().name, | |||
"time": self.next_phase_date.isoformat(), | |||
}, | |||
} | |||
@@ -67,10 +75,7 @@ class Object(Serializable): | |||
An astronomical object. | |||
""" | |||
def __init__(self, | |||
name: str, | |||
skyfield_name: str, | |||
radius: float = None): | |||
def __init__(self, name: str, skyfield_name: str, radius: float = None): | |||
""" | |||
Initialize an astronomical object | |||
@@ -84,7 +89,7 @@ class Object(Serializable): | |||
self.radius = radius | |||
def __repr__(self): | |||
return '<Object type=%s name=%s />' % (self.get_type(), self.name) | |||
return "<Object type=%s name=%s />" % (self.get_type(), self.name) | |||
def get_skyfield_object(self) -> SkfPlanet: | |||
return get_skf_objects()[self.skyfield_name] | |||
@@ -101,41 +106,54 @@ class Object(Serializable): | |||
:return: | |||
""" | |||
if self.radius is None: | |||
raise ValueError('Missing radius for %s object' % self.name) | |||
raise ValueError("Missing radius for %s object" % self.name) | |||
return 360 / pi * arcsin(self.radius / from_place.at(time).observe(self.get_skyfield_object()).distance().km) | |||
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, | |||
"name": self.name, | |||
"type": self.get_type(), | |||
"radius": self.radius, | |||
} | |||
class Star(Object): | |||
def get_type(self) -> str: | |||
return 'star' | |||
return "star" | |||
class Planet(Object): | |||
def get_type(self) -> str: | |||
return 'planet' | |||
return "planet" | |||
class DwarfPlanet(Planet): | |||
def get_type(self) -> str: | |||
return 'dwarf_planet' | |||
return "dwarf_planet" | |||
class Satellite(Object): | |||
def get_type(self) -> str: | |||
return 'satellite' | |||
return "satellite" | |||
class Event(Serializable): | |||
def __init__(self, event_type: EventType, objects: [Object], start_time: datetime, | |||
end_time: Union[datetime, None] = None, details: str = None): | |||
def __init__( | |||
self, | |||
event_type: EventType, | |||
objects: [Object], | |||
start_time: datetime, | |||
end_time: Union[datetime, None] = None, | |||
details: str = None, | |||
): | |||
self.event_type = event_type | |||
self.objects = objects | |||
self.start_time = start_time | |||
@@ -143,16 +161,18 @@ class Event(Serializable): | |||
self.details = details | |||
def __repr__(self): | |||
return '<Event type=%s objects=%s start=%s end=%s details=%s />' % (self.event_type.name, | |||
self.objects, | |||
self.start_time, | |||
self.end_time, | |||
self.details) | |||
return "<Event type=%s objects=%s start=%s end=%s details=%s />" % ( | |||
self.event_type.name, | |||
self.objects, | |||
self.start_time, | |||
self.end_time, | |||
self.details, | |||
) | |||
def get_description(self, show_details: bool = True) -> str: | |||
description = self.event_type.value % self._get_objects_name() | |||
if show_details and self.details is not None: | |||
description += ' ({:s})'.format(self.details) | |||
description += " ({:s})".format(self.details) | |||
return description | |||
def _get_objects_name(self): | |||
@@ -163,20 +183,22 @@ class Event(Serializable): | |||
def serialize(self) -> dict: | |||
return { | |||
'objects': [object.serialize() for object in self.objects], | |||
'EventType': self.event_type.name, | |||
'starts_at': self.start_time.isoformat(), | |||
'ends_at': self.end_time.isoformat() if self.end_time is not None else None, | |||
'details': self.details | |||
"objects": [object.serialize() for object in self.objects], | |||
"EventType": self.event_type.name, | |||
"starts_at": self.start_time.isoformat(), | |||
"ends_at": self.end_time.isoformat() if self.end_time is not None else None, | |||
"details": self.details, | |||
} | |||
class AsterEphemerides(Serializable): | |||
def __init__(self, | |||
rise_time: Union[datetime, None], | |||
culmination_time: Union[datetime, None], | |||
set_time: Union[datetime, None], | |||
aster: Object): | |||
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 | |||
@@ -184,25 +206,33 @@ class AsterEphemerides(Serializable): | |||
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 | |||
"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, | |||
} | |||
EARTH = Planet('Earth', 'EARTH') | |||
EARTH = Planet("Earth", "EARTH") | |||
ASTERS = [Star('Sun', 'SUN', radius=696342), | |||
Satellite('Moon', 'MOON', radius=1737.4), | |||
Planet('Mercury', 'MERCURY', radius=2439.7), | |||
Planet('Venus', 'VENUS', radius=6051.8), | |||
Planet('Mars', 'MARS', radius=3396.2), | |||
Planet('Jupiter', 'JUPITER BARYCENTER', radius=71492), | |||
Planet('Saturn', 'SATURN BARYCENTER', radius=60268), | |||
Planet('Uranus', 'URANUS BARYCENTER', radius=25559), | |||
Planet('Neptune', 'NEPTUNE BARYCENTER', radius=24764), | |||
Planet('Pluto', 'PLUTO BARYCENTER', radius=1185)] | |||
ASTERS = [ | |||
Star("Sun", "SUN", radius=696342), | |||
Satellite("Moon", "MOON", radius=1737.4), | |||
Planet("Mercury", "MERCURY", radius=2439.7), | |||
Planet("Venus", "VENUS", radius=6051.8), | |||
Planet("Mars", "MARS", radius=3396.2), | |||
Planet("Jupiter", "JUPITER BARYCENTER", radius=71492), | |||
Planet("Saturn", "SATURN BARYCENTER", radius=60268), | |||
Planet("Uranus", "URANUS BARYCENTER", radius=25559), | |||
Planet("Neptune", "NEPTUNE BARYCENTER", radius=24764), | |||
Planet("Pluto", "PLUTO BARYCENTER", radius=1185), | |||
] | |||
class Position: | |||
@@ -214,10 +244,11 @@ class Position: | |||
def get_planet_topos(self) -> Topos: | |||
if self.aster is None: | |||
raise TypeError('Observation planet must be set.') | |||
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) | |||
self._topos = self.aster.get_skyfield_object() + Topos( | |||
latitude_degrees=self.latitude, longitude_degrees=self.longitude | |||
) | |||
return self._topos |
@@ -25,4 +25,6 @@ def translate_to_timezone(date: datetime, to_tz: int, from_tz: int = None): | |||
else: | |||
source_tz = timezone.utc | |||
return date.replace(tzinfo=source_tz).astimezone(tz=timezone(timedelta(hours=to_tz))) | |||
return date.replace(tzinfo=source_tz).astimezone( | |||
tz=timezone(timedelta(hours=to_tz)) | |||
) |
@@ -21,6 +21,7 @@ from enum import Enum, auto | |||
class MoonPhaseType(Enum): | |||
"""An enumeration of moon phases.""" | |||
NEW_MOON = 1 | |||
WAXING_CRESCENT = 2 | |||
FIRST_QUARTER = 3 | |||
@@ -33,6 +34,7 @@ class MoonPhaseType(Enum): | |||
class EventType(Enum): | |||
"""An enumeration for the supported event types.""" | |||
OPPOSITION = 1 | |||
CONJUNCTION = 2 | |||
OCCULTATION = 3 | |||
@@ -33,8 +33,12 @@ from .exceptions import OutOfRangeDateError | |||
RISEN_ANGLE = -0.8333 | |||
def _get_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) | |||
def _get_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 | |||
) | |||
phases = list(MoonPhaseType) | |||
current_phase = None | |||
@@ -65,28 +69,34 @@ def _get_skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[ | |||
next_phase_time = times[j] | |||
break | |||
return MoonPhase(current_phase, | |||
current_phase_time.utc_datetime() if current_phase_time is not None else None, | |||
next_phase_time.utc_datetime() if next_phase_time is not None else None) | |||
return MoonPhase( | |||
current_phase, | |||
current_phase_time.utc_datetime() if current_phase_time is not None else None, | |||
next_phase_time.utc_datetime() if next_phase_time is not None else None, | |||
) | |||
def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase: | |||
earth = get_skf_objects()['earth'] | |||
moon = get_skf_objects()['moon'] | |||
sun = get_skf_objects()['sun'] | |||
earth = get_skf_objects()["earth"] | |||
moon = get_skf_objects()["moon"] | |||
sun = get_skf_objects()["sun"] | |||
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') | |||
_, 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) | |||
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) | |||
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 | |||
) | |||
try: | |||
times, phase = find_discrete(time1, time2, moon_phase_at) | |||
@@ -94,7 +104,9 @@ def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase: | |||
start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | |||
end = translate_to_timezone(error.end_time.utc_datetime(), timezone) | |||
start = datetime.date(start.year, start.month, start.day) + datetime.timedelta(days=12) | |||
start = datetime.date(start.year, start.month, start.day) + datetime.timedelta( | |||
days=12 | |||
) | |||
end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12) | |||
raise OutOfRangeDateError(start, end) from error | |||
@@ -102,31 +114,51 @@ def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase: | |||
return _get_skyfield_to_moon_phase(times, phase, today) | |||
def get_ephemerides(date: datetime.date, position: Position, timezone: int = 0) -> [AsterEphemerides]: | |||
def get_ephemerides( | |||
date: datetime.date, position: Position, timezone: int = 0 | |||
) -> [AsterEphemerides]: | |||
ephemerides = [] | |||
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 | |||
return ( | |||
position.get_planet_topos() | |||
.at(time) | |||
.observe(for_aster.get_skyfield_object()) | |||
.apparent() | |||
.altaz()[0] | |||
.degrees | |||
) | |||
fun.rough_period = 1.0 | |||
return fun | |||
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 | |||
start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) | |||
end_time = get_timescale().utc(date.year, date.month, date.day, 23 - timezone, 59, 59) | |||
end_time = get_timescale().utc( | |||
date.year, date.month, date.day, 23 - timezone, 59, 59 | |||
) | |||
try: | |||
for aster in ASTERS: | |||
rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) | |||
try: | |||
culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12) | |||
culmination_time = culmination_time[0] if len(culmination_time) > 0 else None | |||
culmination_time, _ = find_maxima( | |||
start_time, | |||
end_time, | |||
f=get_angle(aster), | |||
epsilon=1.0 / 3600 / 24, | |||
num=12, | |||
) | |||
culmination_time = ( | |||
culmination_time[0] if len(culmination_time) > 0 else None | |||
) | |||
except ValueError: | |||
culmination_time = None | |||
@@ -139,18 +171,24 @@ def get_ephemerides(date: datetime.date, position: Position, timezone: int = 0) | |||
# Convert the Time instances to Python datetime objects | |||
if rise_time is not None: | |||
rise_time = translate_to_timezone(rise_time.utc_datetime().replace(microsecond=0), | |||
to_tz=timezone) | |||
rise_time = translate_to_timezone( | |||
rise_time.utc_datetime().replace(microsecond=0), to_tz=timezone | |||
) | |||
if culmination_time is not None: | |||
culmination_time = translate_to_timezone(culmination_time.utc_datetime().replace(microsecond=0), | |||
to_tz=timezone) | |||
culmination_time = translate_to_timezone( | |||
culmination_time.utc_datetime().replace(microsecond=0), | |||
to_tz=timezone, | |||
) | |||
if set_time is not None: | |||
set_time = translate_to_timezone(set_time.utc_datetime().replace(microsecond=0), | |||
to_tz=timezone) | |||
set_time = translate_to_timezone( | |||
set_time.utc_datetime().replace(microsecond=0), to_tz=timezone | |||
) | |||
ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster)) | |||
ephemerides.append( | |||
AsterEphemerides(rise_time, culmination_time, set_time, aster=aster) | |||
) | |||
except EphemerisRangeError as error: | |||
start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | |||
end = translate_to_timezone(error.end_time.utc_datetime(), timezone) | |||
@@ -31,16 +31,22 @@ from .core import get_timescale, get_skf_objects, flatten_list | |||
def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Event]: | |||
earth = get_skf_objects()['earth'] | |||
earth = get_skf_objects()["earth"] | |||
aster1 = None | |||
aster2 = None | |||
def is_in_conjunction(time: Time): | |||
earth_pos = earth.at(time) | |||
_, aster1_lon, _ = earth_pos.observe(aster1.get_skyfield_object()).apparent().ecliptic_latlon() | |||
_, aster2_lon, _ = earth_pos.observe(aster2.get_skyfield_object()).apparent().ecliptic_latlon() | |||
_, aster1_lon, _ = ( | |||
earth_pos.observe(aster1.get_skyfield_object()).apparent().ecliptic_latlon() | |||
) | |||
_, aster2_lon, _ = ( | |||
earth_pos.observe(aster2.get_skyfield_object()).apparent().ecliptic_latlon() | |||
) | |||
return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype('int8') == 0 | |||
return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype( | |||
"int8" | |||
) == 0 | |||
is_in_conjunction.rough_period = 60.0 | |||
@@ -64,16 +70,30 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
aster2_pos = (aster2.get_skyfield_object() - earth).at(time) | |||
distance = aster1_pos.separation_from(aster2_pos).degrees | |||
if distance - aster2.get_apparent_radius(time, earth) < aster1.get_apparent_radius(time, earth): | |||
occulting_aster = [aster1, | |||
aster2] if aster1_pos.distance().km < aster2_pos.distance().km else [aster2, | |||
aster1] | |||
conjunctions.append(Event(EventType.OCCULTATION, occulting_aster, | |||
translate_to_timezone(time.utc_datetime(), timezone))) | |||
if distance - aster2.get_apparent_radius( | |||
time, earth | |||
) < aster1.get_apparent_radius(time, earth): | |||
occulting_aster = ( | |||
[aster1, aster2] | |||
if aster1_pos.distance().km < aster2_pos.distance().km | |||
else [aster2, aster1] | |||
) | |||
conjunctions.append( | |||
Event( | |||
EventType.OCCULTATION, | |||
occulting_aster, | |||
translate_to_timezone(time.utc_datetime(), timezone), | |||
) | |||
) | |||
else: | |||
conjunctions.append(Event(EventType.CONJUNCTION, [aster1, aster2], | |||
translate_to_timezone(time.utc_datetime(), timezone))) | |||
conjunctions.append( | |||
Event( | |||
EventType.CONJUNCTION, | |||
[aster1, aster2], | |||
translate_to_timezone(time.utc_datetime(), timezone), | |||
) | |||
) | |||
computed.append(aster1) | |||
@@ -81,13 +101,15 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Event]: | |||
earth = get_skf_objects()['earth'] | |||
sun = get_skf_objects()['sun'] | |||
earth = get_skf_objects()["earth"] | |||
sun = get_skf_objects()["sun"] | |||
aster = None | |||
def is_oppositing(time: Time) -> [bool]: | |||
earth_pos = earth.at(time) | |||
sun_pos = earth_pos.observe(sun).apparent() # Never do this without eyes protection! | |||
sun_pos = earth_pos.observe( | |||
sun | |||
).apparent() # Never do this without eyes protection! | |||
aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent() | |||
_, lon1, _ = sun_pos.ecliptic_latlon() | |||
_, lon2, _ = aster_pos.ecliptic_latlon() | |||
@@ -97,19 +119,27 @@ def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
events = [] | |||
for aster in ASTERS: | |||
if not isinstance(aster, Planet) or aster.skyfield_name in ['MERCURY', 'VENUS']: | |||
if not isinstance(aster, Planet) or aster.skyfield_name in ["MERCURY", "VENUS"]: | |||
continue | |||
times, _ = find_discrete(start_time, end_time, is_oppositing) | |||
for time in times: | |||
events.append(Event(EventType.OPPOSITION, [aster], translate_to_timezone(time.utc_datetime(), timezone))) | |||
events.append( | |||
Event( | |||
EventType.OPPOSITION, | |||
[aster], | |||
translate_to_timezone(time.utc_datetime(), timezone), | |||
) | |||
) | |||
return events | |||
def _search_maximal_elongations(start_time: Time, end_time: Time, timezone: int) -> [Event]: | |||
earth = get_skf_objects()['earth'] | |||
sun = get_skf_objects()['sun'] | |||
def _search_maximal_elongations( | |||
start_time: Time, end_time: Time, timezone: int | |||
) -> [Event]: | |||
earth = get_skf_objects()["earth"] | |||
sun = get_skf_objects()["sun"] | |||
aster = None | |||
def get_elongation(time: Time): | |||
@@ -123,24 +153,30 @@ def _search_maximal_elongations(start_time: Time, end_time: Time, timezone: int) | |||
events = [] | |||
for aster in ASTERS: | |||
if aster.skyfield_name not in ['MERCURY', 'VENUS']: | |||
if aster.skyfield_name not in ["MERCURY", "VENUS"]: | |||
continue | |||
times, elongations = find_maxima(start_time, end_time, f=get_elongation, epsilon=1./24/3600, num=12) | |||
times, elongations = find_maxima( | |||
start_time, end_time, f=get_elongation, epsilon=1.0 / 24 / 3600, num=12 | |||
) | |||
for i, time in enumerate(times): | |||
elongation = elongations[i] | |||
events.append(Event(EventType.MAXIMAL_ELONGATION, | |||
[aster], | |||
translate_to_timezone(time.utc_datetime(), timezone), | |||
details='{:.3n}°'.format(elongation))) | |||
events.append( | |||
Event( | |||
EventType.MAXIMAL_ELONGATION, | |||
[aster], | |||
translate_to_timezone(time.utc_datetime(), timezone), | |||
details="{:.3n}°".format(elongation), | |||
) | |||
) | |||
return events | |||
def _get_moon_distance(): | |||
earth = get_skf_objects()['earth'] | |||
moon = get_skf_objects()['moon'] | |||
earth = get_skf_objects()["earth"] | |||
moon = get_skf_objects()["moon"] | |||
def get_distance(time: Time): | |||
earth_pos = earth.at(time) | |||
@@ -157,10 +193,18 @@ def _search_moon_apogee(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
moon = ASTERS[1] | |||
events = [] | |||
times, _ = find_maxima(start_time, end_time, f=_get_moon_distance(), epsilon=1./24/60) | |||
times, _ = find_maxima( | |||
start_time, end_time, f=_get_moon_distance(), epsilon=1.0 / 24 / 60 | |||
) | |||
for time in times: | |||
events.append(Event(EventType.MOON_APOGEE, [moon], translate_to_timezone(time.utc_datetime(), timezone))) | |||
events.append( | |||
Event( | |||
EventType.MOON_APOGEE, | |||
[moon], | |||
translate_to_timezone(time.utc_datetime(), timezone), | |||
) | |||
) | |||
return events | |||
@@ -169,10 +213,18 @@ def _search_moon_perigee(start_time: Time, end_time: Time, timezone: int) -> [Ev | |||
moon = ASTERS[1] | |||
events = [] | |||
times, _ = find_minima(start_time, end_time, f=_get_moon_distance(), epsilon=1./24/60) | |||
times, _ = find_minima( | |||
start_time, end_time, f=_get_moon_distance(), epsilon=1.0 / 24 / 60 | |||
) | |||
for time in times: | |||
events.append(Event(EventType.MOON_PERIGEE, [moon], translate_to_timezone(time.utc_datetime(), timezone))) | |||
events.append( | |||
Event( | |||
EventType.MOON_PERIGEE, | |||
[moon], | |||
translate_to_timezone(time.utc_datetime(), timezone), | |||
) | |||
) | |||
return events | |||
@@ -206,11 +258,13 @@ def get_events(date: date_type, timezone: int = 0) -> [Event]: | |||
try: | |||
found_events = [] | |||
for fun in [_search_oppositions, | |||
_search_conjunction, | |||
_search_maximal_elongations, | |||
_search_moon_apogee, | |||
_search_moon_perigee]: | |||
for fun in [ | |||
_search_oppositions, | |||
_search_conjunction, | |||
_search_maximal_elongations, | |||
_search_moon_apogee, | |||
_search_moon_perigee, | |||
]: | |||
found_events.append(fun(start_time, end_time, timezone)) | |||
return sorted(flatten_list(found_events), key=lambda event: event.start_time) | |||
@@ -30,8 +30,10 @@ class OutOfRangeDateError(RuntimeError): | |||
super().__init__() | |||
self.min_date = min_date | |||
self.max_date = max_date | |||
self.msg = 'The date must be between %s and %s' % (min_date.strftime('%Y-%m-%d'), | |||
max_date.strftime('%Y-%m-%d')) | |||
self.msg = "The date must be between %s and %s" % ( | |||
min_date.strftime("%Y-%m-%d"), | |||
max_date.strftime("%Y-%m-%d"), | |||
) | |||
class CompileError(RuntimeError): | |||
@@ -9,31 +9,42 @@ from dateutil.relativedelta import relativedelta | |||
class CoreTestCase(unittest.TestCase): | |||
def test_flatten_list(self): | |||
self.assertEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], core.flatten_list([0, 1, 2, [3, 4, [5, 6], 7], 8, [9]])) | |||
self.assertEqual( | |||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], | |||
core.flatten_list([0, 1, 2, [3, 4, [5, 6], 7], 8, [9]]), | |||
) | |||
def test_get_env(self): | |||
self.assertEqual(0, len(core.get_env())) | |||
os.environ['SOME_RANDOM_VAR'] = 'an awesome value' | |||
os.environ["SOME_RANDOM_VAR"] = "an awesome value" | |||
self.assertEqual(0, len(core.get_env())) | |||
os.environ['KOSMORRO_GREAT_VARIABLE'] = 'value' | |||
os.environ["KOSMORRO_GREAT_VARIABLE"] = "value" | |||
env = core.get_env() | |||
self.assertEqual(1, len(env)) | |||
self.assertEqual('value', env.great_variable) | |||
self.assertEqual("value", env.great_variable) | |||
os.environ['KOSMORRO_ANOTHER_VARIABLE'] = 'another value' | |||
os.environ["KOSMORRO_ANOTHER_VARIABLE"] = "another value" | |||
env = core.get_env() | |||
self.assertEqual(2, len(env)) | |||
self.assertEqual('value', env.great_variable) | |||
self.assertEqual('another value', env.another_variable) | |||
self.assertEqual("value", env.great_variable) | |||
self.assertEqual("another value", env.another_variable) | |||
self.assertEqual("{'great_variable': 'value', 'another_variable': 'another value'}", str(env)) | |||
self.assertEqual( | |||
"{'great_variable': 'value', 'another_variable': 'another value'}", str(env) | |||
) | |||
def test_date_arg_parsing(self): | |||
self.assertEqual(core.get_date("+1y 2m3d"), date.today() + relativedelta(years=1, months=2, days=3)) | |||
self.assertEqual(core.get_date("-1y2d"), date.today() - relativedelta(years=1, days=2)) | |||
self.assertEqual( | |||
core.get_date("+1y 2m3d"), | |||
date.today() + relativedelta(years=1, months=2, days=3), | |||
) | |||
self.assertEqual( | |||
core.get_date("-1y2d"), date.today() - relativedelta(years=1, days=2) | |||
) | |||
self.assertEqual(core.get_date("1111-11-13"), date(1111, 11, 13)) | |||
if __name__ == '__main__': | |||
if __name__ == "__main__": | |||
unittest.main() |
@@ -5,13 +5,15 @@ from kosmorrolib import data, core | |||
class DataTestCase(unittest.TestCase): | |||
def test_object_radius_must_be_set_to_get_apparent_radius(self): | |||
o = data.Planet('Saturn', 'SATURN') | |||
o = data.Planet("Saturn", "SATURN") | |||
with self.assertRaises(ValueError) as context: | |||
o.get_apparent_radius(core.get_timescale().now(), core.get_skf_objects()['earth']) | |||
o.get_apparent_radius( | |||
core.get_timescale().now(), core.get_skf_objects()["earth"] | |||
) | |||
self.assertEqual(('Missing radius for Saturn object',), context.exception.args) | |||
self.assertEqual(("Missing radius for Saturn object",), context.exception.args) | |||
if __name__ == '__main__': | |||
if __name__ == "__main__": | |||
unittest.main() |
@@ -12,13 +12,17 @@ class DateUtilTestCase(unittest.TestCase): | |||
date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 0), to_tz=2) | |||
self.assertEqual(2, date.hour) | |||
date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 8), to_tz=2, from_tz=6) | |||
date = dateutil.translate_to_timezone( | |||
datetime(2020, 6, 9, 8), to_tz=2, from_tz=6 | |||
) | |||
self.assertEqual(4, date.hour) | |||
date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 1), to_tz=0, from_tz=2) | |||
date = dateutil.translate_to_timezone( | |||
datetime(2020, 6, 9, 1), to_tz=0, from_tz=2 | |||
) | |||
self.assertEqual(8, date.day) | |||
self.assertEqual(23, date.hour) | |||
if __name__ == '__main__': | |||
if __name__ == "__main__": | |||
unittest.main() |
@@ -12,16 +12,17 @@ from kosmorrolib.exceptions import OutOfRangeDateError | |||
class EphemeridesTestCase(unittest.TestCase): | |||
def test_get_ephemerides_for_aster_returns_correct_hours(self): | |||
position = Position(0, 0, EARTH) | |||
eph = ephemerides.get_ephemerides(date=date(2019, 11, 18), | |||
position=position) | |||
eph = ephemerides.get_ephemerides(date=date(2019, 11, 18), position=position) | |||
@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:') | |||
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() | |||
@@ -34,61 +35,61 @@ class EphemeridesTestCase(unittest.TestCase): | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 25)) | |||
self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-26T") | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 26)) | |||
self.assertEqual(MoonPhaseType.NEW_MOON, phase.phase_type) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-12-04T") | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 27)) | |||
self.assertEqual(MoonPhaseType.WAXING_CRESCENT, phase.phase_type) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-12-04T") | |||
def test_moon_phase_first_crescent(self): | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 3)) | |||
self.assertEqual(MoonPhaseType.WAXING_CRESCENT, phase.phase_type) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-04T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-04T") | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 4)) | |||
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.phase_type) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-12T") | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 5)) | |||
self.assertEqual(MoonPhaseType.WAXING_GIBBOUS, phase.phase_type) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-12T") | |||
def test_moon_phase_full_moon(self): | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 11)) | |||
self.assertEqual(MoonPhaseType.WAXING_GIBBOUS, phase.phase_type) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-12T") | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 12)) | |||
self.assertEqual(MoonPhaseType.FULL_MOON, phase.phase_type) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-19T") | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 13)) | |||
self.assertEqual(MoonPhaseType.WANING_GIBBOUS, phase.phase_type) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-19T") | |||
def test_moon_phase_last_quarter(self): | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 18)) | |||
self.assertEqual(MoonPhaseType.WANING_GIBBOUS, phase.phase_type) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-19T") | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 19)) | |||
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.phase_type) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-26T") | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 20)) | |||
self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-26T") | |||
def test_moon_phase_prediction(self): | |||
phase = MoonPhase(MoonPhaseType.NEW_MOON, None, None) | |||
@@ -120,5 +121,5 @@ class EphemeridesTestCase(unittest.TestCase): | |||
ephemerides.get_moon_phase(date(1789, 5, 5)) | |||
if __name__ == '__main__': | |||
if __name__ == "__main__": | |||
unittest.main() |
@@ -10,43 +10,111 @@ from kosmorrolib.exceptions import OutOfRangeDateError | |||
EXPECTED_EVENTS = [ | |||
(date(2020, 2, 7), []), | |||
(date(2020, 10, 13), [Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2020, 10, 13, 23, 25))]), | |||
(date(2022, 12, 8), | |||
[Event(EventType.CONJUNCTION, [ASTERS[1], ASTERS[4]], datetime(2022, 12, 8, 4, 18)), | |||
Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2022, 12, 8, 5, 41))]), | |||
(date(2025, 1, 16), [Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2025, 1, 16, 2, 38))]), | |||
(date(2027, 2, 19), [Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2027, 2, 19, 7, 38)), | |||
Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2027, 2, 19, 15, 50))]), | |||
(date(2020, 1, 2), [Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 1, 2, 1, 32)), | |||
Event(EventType.CONJUNCTION, [ASTERS[2], ASTERS[5]], | |||
datetime(2020, 1, 2, 16, 41))]), | |||
(date(2020, 1, 12), | |||
[Event(EventType.CONJUNCTION, [ASTERS[2], ASTERS[6]], datetime(2020, 1, 12, 9, 51)), | |||
Event(EventType.CONJUNCTION, [ASTERS[2], ASTERS[9]], datetime(2020, 1, 12, 10, 13)), | |||
Event(EventType.CONJUNCTION, [ASTERS[6], ASTERS[9]], datetime(2020, 1, 12, 16, 57))]), | |||
(date(2020, 2, 10), | |||
[Event(EventType.MAXIMAL_ELONGATION, [ASTERS[2]], datetime(2020, 2, 10, 13, 46), details='18.2°'), | |||
Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2020, 2, 10, 20, 34))]), | |||
(date(2020, 3, 24), | |||
[Event(EventType.MAXIMAL_ELONGATION, [ASTERS[2]], datetime(2020, 3, 24, 1, 56), details='27.8°'), | |||
Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 3, 24, 15, 39)), | |||
Event(EventType.MAXIMAL_ELONGATION, [ASTERS[3]], datetime(2020, 3, 24, 21, 58), | |||
details='46.1°')]), | |||
(date(2005, 6, 16), | |||
[Event(EventType.OCCULTATION, [ASTERS[1], ASTERS[5]], datetime(2005, 6, 16, 6, 31))]), | |||
(date(2020, 4, 7), [Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2020, 4, 7, 18, 14))]), | |||
(date(2020, 1, 29), [Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 1, 29, 21, 32))]) | |||
( | |||
date(2020, 10, 13), | |||
[Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2020, 10, 13, 23, 25))], | |||
), | |||
( | |||
date(2022, 12, 8), | |||
[ | |||
Event( | |||
EventType.CONJUNCTION, | |||
[ASTERS[1], ASTERS[4]], | |||
datetime(2022, 12, 8, 4, 18), | |||
), | |||
Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2022, 12, 8, 5, 41)), | |||
], | |||
), | |||
( | |||
date(2025, 1, 16), | |||
[Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2025, 1, 16, 2, 38))], | |||
), | |||
( | |||
date(2027, 2, 19), | |||
[ | |||
Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2027, 2, 19, 7, 38)), | |||
Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2027, 2, 19, 15, 50)), | |||
], | |||
), | |||
( | |||
date(2020, 1, 2), | |||
[ | |||
Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 1, 2, 1, 32)), | |||
Event( | |||
EventType.CONJUNCTION, | |||
[ASTERS[2], ASTERS[5]], | |||
datetime(2020, 1, 2, 16, 41), | |||
), | |||
], | |||
), | |||
( | |||
date(2020, 1, 12), | |||
[ | |||
Event( | |||
EventType.CONJUNCTION, | |||
[ASTERS[2], ASTERS[6]], | |||
datetime(2020, 1, 12, 9, 51), | |||
), | |||
Event( | |||
EventType.CONJUNCTION, | |||
[ASTERS[2], ASTERS[9]], | |||
datetime(2020, 1, 12, 10, 13), | |||
), | |||
Event( | |||
EventType.CONJUNCTION, | |||
[ASTERS[6], ASTERS[9]], | |||
datetime(2020, 1, 12, 16, 57), | |||
), | |||
], | |||
), | |||
( | |||
date(2020, 2, 10), | |||
[ | |||
Event( | |||
EventType.MAXIMAL_ELONGATION, | |||
[ASTERS[2]], | |||
datetime(2020, 2, 10, 13, 46), | |||
details="18.2°", | |||
), | |||
Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2020, 2, 10, 20, 34)), | |||
], | |||
), | |||
( | |||
date(2020, 3, 24), | |||
[ | |||
Event( | |||
EventType.MAXIMAL_ELONGATION, | |||
[ASTERS[2]], | |||
datetime(2020, 3, 24, 1, 56), | |||
details="27.8°", | |||
), | |||
Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 3, 24, 15, 39)), | |||
Event( | |||
EventType.MAXIMAL_ELONGATION, | |||
[ASTERS[3]], | |||
datetime(2020, 3, 24, 21, 58), | |||
details="46.1°", | |||
), | |||
], | |||
), | |||
( | |||
date(2005, 6, 16), | |||
[ | |||
Event( | |||
EventType.OCCULTATION, | |||
[ASTERS[1], ASTERS[5]], | |||
datetime(2005, 6, 16, 6, 31), | |||
) | |||
], | |||
), | |||
( | |||
date(2020, 4, 7), | |||
[Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2020, 4, 7, 18, 14))], | |||
), | |||
( | |||
date(2020, 1, 29), | |||
[Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 1, 29, 21, 32))], | |||
), | |||
] | |||
@@ -57,20 +125,23 @@ class EventTestCase(unittest.TestCase): | |||
@parameterized.expand(EXPECTED_EVENTS) | |||
def test_search_events(self, d: date, expected_events: [Event]): | |||
actual_events = events.get_events(d) | |||
self.assertEqual(len(expected_events), len(actual_events), | |||
'Expected %d elements, got %d for date %s.\n%s' % (len(expected_events), | |||
len(actual_events), | |||
d.isoformat(), | |||
actual_events)) | |||
self.assertEqual( | |||
len(expected_events), | |||
len(actual_events), | |||
"Expected %d elements, got %d for date %s.\n%s" | |||
% (len(expected_events), len(actual_events), d.isoformat(), actual_events), | |||
) | |||
for i, expected_event in enumerate(expected_events): | |||
actual_event = actual_events[i] | |||
# Remove unnecessary precision (seconds and microseconds) | |||
actual_event.start_time = datetime(actual_event.start_time.year, | |||
actual_event.start_time.month, | |||
actual_event.start_time.day, | |||
actual_event.start_time.hour, | |||
actual_event.start_time.minute) | |||
actual_event.start_time = datetime( | |||
actual_event.start_time.year, | |||
actual_event.start_time.month, | |||
actual_event.start_time.day, | |||
actual_event.start_time.hour, | |||
actual_event.start_time.minute, | |||
) | |||
self.assertEqual(expected_event.__dict__, actual_event.__dict__) | |||
@@ -79,5 +150,5 @@ class EventTestCase(unittest.TestCase): | |||
events.get_events(date(1789, 5, 5)) | |||
if __name__ == '__main__': | |||
if __name__ == "__main__": | |||
unittest.main() |
@@ -39,9 +39,10 @@ def expect_assertions(assert_fun, num=1): | |||
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)) | |||
raise AssertionError( | |||
"Expected %d call(s) to function %s but called %d time(s)." | |||
% (num, assert_fun.__name__, count) | |||
) | |||
return sniff_function | |||