@@ -1,3 +1,6 @@ | |||||
black: | |||||
pipenv run black kosmorrolib tests | |||||
.PHONY: test | .PHONY: test | ||||
tests: legacy-tests | tests: legacy-tests | ||||
python3 tests.py | 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 | __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.timelib import Time | ||||
from skyfield.nutationlib import iau2000b | from skyfield.nutationlib import iau2000b | ||||
CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache' | |||||
CACHE_FOLDER = str(Path.home()) + "/.kosmorro-cache" | |||||
class Environment: | class Environment: | ||||
def __init__(self): | def __init__(self): | ||||
@@ -46,18 +47,20 @@ class Environment: | |||||
def __len__(self): | def __len__(self): | ||||
return len(self._vars) | return len(self._vars) | ||||
def get_env() -> Environment: | def get_env() -> Environment: | ||||
environment = Environment() | environment = Environment() | ||||
for var in os.environ: | for var in os.environ: | ||||
if not re.search('^KOSMORRO_', var): | |||||
if not re.search("^KOSMORRO_", var): | |||||
continue | continue | ||||
[_, env] = var.split('_', 1) | |||||
[_, env] = var.split("_", 1) | |||||
environment.__set__(env.lower(), os.getenv(var)) | environment.__set__(env.lower(), os.getenv(var)) | ||||
return environment | return environment | ||||
def get_loader(): | def get_loader(): | ||||
return Loader(CACHE_FOLDER) | return Loader(CACHE_FOLDER) | ||||
@@ -67,7 +70,7 @@ def get_timescale(): | |||||
def get_skf_objects(): | def get_skf_objects(): | ||||
return get_loader()('de421.bsp') | |||||
return get_loader()("de421.bsp") | |||||
def get_iau2000b(time: Time): | def get_iau2000b(time: Time): | ||||
@@ -92,26 +95,32 @@ def flatten_list(the_list: list): | |||||
def get_date(date_arg: str) -> date: | 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: | try: | ||||
return date.fromisoformat(date_arg) | return date.fromisoformat(date_arg) | ||||
except ValueError as error: | 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): | 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 | 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) | ||||
return date.today() - relativedelta(days=days, months=months, years=years) | return date.today() - relativedelta(days=days, months=months, years=years) | ||||
else: | 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)) | raise ValueError(error_msg.format(date=date_arg)) |
@@ -36,7 +36,12 @@ class Serializable(ABC): | |||||
class MoonPhase(Serializable): | 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.phase_type = phase_type | ||||
self.time = time | self.time = time | ||||
self.next_phase_date = next_phase_date | self.next_phase_date = next_phase_date | ||||
@@ -44,7 +49,10 @@ class MoonPhase(Serializable): | |||||
def get_next_phase(self): | def get_next_phase(self): | ||||
if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]: | if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]: | ||||
return MoonPhaseType.FIRST_QUARTER | 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 | return MoonPhaseType.FULL_MOON | ||||
if self.phase_type in [MoonPhaseType.FULL_MOON, MoonPhaseType.WANING_GIBBOUS]: | if self.phase_type in [MoonPhaseType.FULL_MOON, MoonPhaseType.WANING_GIBBOUS]: | ||||
return MoonPhaseType.LAST_QUARTER | return MoonPhaseType.LAST_QUARTER | ||||
@@ -53,12 +61,12 @@ class MoonPhase(Serializable): | |||||
def serialize(self) -> dict: | def serialize(self) -> dict: | ||||
return { | 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. | 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 | Initialize an astronomical object | ||||
@@ -84,7 +89,7 @@ class Object(Serializable): | |||||
self.radius = radius | self.radius = radius | ||||
def __repr__(self): | 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: | def get_skyfield_object(self) -> SkfPlanet: | ||||
return get_skf_objects()[self.skyfield_name] | return get_skf_objects()[self.skyfield_name] | ||||
@@ -101,41 +106,54 @@ class Object(Serializable): | |||||
:return: | :return: | ||||
""" | """ | ||||
if self.radius is None: | 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: | def serialize(self) -> dict: | ||||
return { | return { | ||||
'name': self.name, | |||||
'type': self.get_type(), | |||||
'radius': self.radius, | |||||
"name": self.name, | |||||
"type": self.get_type(), | |||||
"radius": self.radius, | |||||
} | } | ||||
class Star(Object): | class Star(Object): | ||||
def get_type(self) -> str: | def get_type(self) -> str: | ||||
return 'star' | |||||
return "star" | |||||
class Planet(Object): | class Planet(Object): | ||||
def get_type(self) -> str: | def get_type(self) -> str: | ||||
return 'planet' | |||||
return "planet" | |||||
class DwarfPlanet(Planet): | class DwarfPlanet(Planet): | ||||
def get_type(self) -> str: | def get_type(self) -> str: | ||||
return 'dwarf_planet' | |||||
return "dwarf_planet" | |||||
class Satellite(Object): | class Satellite(Object): | ||||
def get_type(self) -> str: | def get_type(self) -> str: | ||||
return 'satellite' | |||||
return "satellite" | |||||
class Event(Serializable): | 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.event_type = event_type | ||||
self.objects = objects | self.objects = objects | ||||
self.start_time = start_time | self.start_time = start_time | ||||
@@ -143,16 +161,18 @@ class Event(Serializable): | |||||
self.details = details | self.details = details | ||||
def __repr__(self): | 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: | def get_description(self, show_details: bool = True) -> str: | ||||
description = self.event_type.value % self._get_objects_name() | description = self.event_type.value % self._get_objects_name() | ||||
if show_details and self.details is not None: | if show_details and self.details is not None: | ||||
description += ' ({:s})'.format(self.details) | |||||
description += " ({:s})".format(self.details) | |||||
return description | return description | ||||
def _get_objects_name(self): | def _get_objects_name(self): | ||||
@@ -163,20 +183,22 @@ class Event(Serializable): | |||||
def serialize(self) -> dict: | def serialize(self) -> dict: | ||||
return { | 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): | 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.rise_time = rise_time | ||||
self.culmination_time = culmination_time | self.culmination_time = culmination_time | ||||
self.set_time = set_time | self.set_time = set_time | ||||
@@ -184,25 +206,33 @@ class AsterEphemerides(Serializable): | |||||
def serialize(self) -> dict: | def serialize(self) -> dict: | ||||
return { | 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: | class Position: | ||||
@@ -214,10 +244,11 @@ class Position: | |||||
def get_planet_topos(self) -> Topos: | def get_planet_topos(self) -> Topos: | ||||
if self.aster is None: | if self.aster is None: | ||||
raise TypeError('Observation planet must be set.') | |||||
raise TypeError("Observation planet must be set.") | |||||
if self._topos is None: | 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 | return self._topos |
@@ -25,4 +25,6 @@ def translate_to_timezone(date: datetime, to_tz: int, from_tz: int = None): | |||||
else: | else: | ||||
source_tz = timezone.utc | 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): | class MoonPhaseType(Enum): | ||||
"""An enumeration of moon phases.""" | """An enumeration of moon phases.""" | ||||
NEW_MOON = 1 | NEW_MOON = 1 | ||||
WAXING_CRESCENT = 2 | WAXING_CRESCENT = 2 | ||||
FIRST_QUARTER = 3 | FIRST_QUARTER = 3 | ||||
@@ -33,6 +34,7 @@ class MoonPhaseType(Enum): | |||||
class EventType(Enum): | class EventType(Enum): | ||||
"""An enumeration for the supported event types.""" | """An enumeration for the supported event types.""" | ||||
OPPOSITION = 1 | OPPOSITION = 1 | ||||
CONJUNCTION = 2 | CONJUNCTION = 2 | ||||
OCCULTATION = 3 | OCCULTATION = 3 | ||||
@@ -33,8 +33,12 @@ from .exceptions import OutOfRangeDateError | |||||
RISEN_ANGLE = -0.8333 | 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) | phases = list(MoonPhaseType) | ||||
current_phase = None | 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] | next_phase_time = times[j] | ||||
break | 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: | 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): | def moon_phase_at(time: Time): | ||||
time._nutation_angles = get_iau2000b(time) | time._nutation_angles = get_iau2000b(time) | ||||
current_earth = earth.at(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) | return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) | ||||
moon_phase_at.rough_period = 7.0 # one lunar phase per week | 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) | 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: | try: | ||||
times, phase = find_discrete(time1, time2, moon_phase_at) | 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) | start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | ||||
end = translate_to_timezone(error.end_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) | end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12) | ||||
raise OutOfRangeDateError(start, end) from error | 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) | 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 = [] | ephemerides = [] | ||||
def get_angle(for_aster: Object): | def get_angle(for_aster: Object): | ||||
def fun(time: Time) -> float: | 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 | fun.rough_period = 1.0 | ||||
return fun | return fun | ||||
def is_risen(for_aster: Object): | def is_risen(for_aster: Object): | ||||
def fun(time: Time) -> bool: | def fun(time: Time) -> bool: | ||||
return get_angle(for_aster)(time) > RISEN_ANGLE | return get_angle(for_aster)(time) > RISEN_ANGLE | ||||
fun.rough_period = 0.5 | fun.rough_period = 0.5 | ||||
return fun | return fun | ||||
start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) | 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: | try: | ||||
for aster in ASTERS: | for aster in ASTERS: | ||||
rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) | rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) | ||||
try: | 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: | except ValueError: | ||||
culmination_time = None | 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 | # Convert the Time instances to Python datetime objects | ||||
if rise_time is not None: | 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: | 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: | 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: | except EphemerisRangeError as error: | ||||
start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | ||||
end = translate_to_timezone(error.end_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]: | def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Event]: | ||||
earth = get_skf_objects()['earth'] | |||||
earth = get_skf_objects()["earth"] | |||||
aster1 = None | aster1 = None | ||||
aster2 = None | aster2 = None | ||||
def is_in_conjunction(time: Time): | def is_in_conjunction(time: Time): | ||||
earth_pos = earth.at(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 | 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) | aster2_pos = (aster2.get_skyfield_object() - earth).at(time) | ||||
distance = aster1_pos.separation_from(aster2_pos).degrees | 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: | 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) | 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]: | 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 | aster = None | ||||
def is_oppositing(time: Time) -> [bool]: | def is_oppositing(time: Time) -> [bool]: | ||||
earth_pos = earth.at(time) | 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() | aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent() | ||||
_, lon1, _ = sun_pos.ecliptic_latlon() | _, lon1, _ = sun_pos.ecliptic_latlon() | ||||
_, lon2, _ = aster_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 = [] | events = [] | ||||
for aster in ASTERS: | 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 | continue | ||||
times, _ = find_discrete(start_time, end_time, is_oppositing) | times, _ = find_discrete(start_time, end_time, is_oppositing) | ||||
for time in times: | 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 | 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 | aster = None | ||||
def get_elongation(time: Time): | def get_elongation(time: Time): | ||||
@@ -123,24 +153,30 @@ def _search_maximal_elongations(start_time: Time, end_time: Time, timezone: int) | |||||
events = [] | events = [] | ||||
for aster in ASTERS: | for aster in ASTERS: | ||||
if aster.skyfield_name not in ['MERCURY', 'VENUS']: | |||||
if aster.skyfield_name not in ["MERCURY", "VENUS"]: | |||||
continue | 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): | for i, time in enumerate(times): | ||||
elongation = elongations[i] | 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 | return events | ||||
def _get_moon_distance(): | 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): | def get_distance(time: Time): | ||||
earth_pos = earth.at(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] | moon = ASTERS[1] | ||||
events = [] | 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: | 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 | return events | ||||
@@ -169,10 +213,18 @@ def _search_moon_perigee(start_time: Time, end_time: Time, timezone: int) -> [Ev | |||||
moon = ASTERS[1] | moon = ASTERS[1] | ||||
events = [] | 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: | 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 | return events | ||||
@@ -206,11 +258,13 @@ def get_events(date: date_type, timezone: int = 0) -> [Event]: | |||||
try: | try: | ||||
found_events = [] | 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)) | found_events.append(fun(start_time, end_time, timezone)) | ||||
return sorted(flatten_list(found_events), key=lambda event: event.start_time) | return sorted(flatten_list(found_events), key=lambda event: event.start_time) | ||||
@@ -30,8 +30,10 @@ class OutOfRangeDateError(RuntimeError): | |||||
super().__init__() | super().__init__() | ||||
self.min_date = min_date | self.min_date = min_date | ||||
self.max_date = max_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): | class CompileError(RuntimeError): | ||||
@@ -9,31 +9,42 @@ from dateutil.relativedelta import relativedelta | |||||
class CoreTestCase(unittest.TestCase): | class CoreTestCase(unittest.TestCase): | ||||
def test_flatten_list(self): | 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): | def test_get_env(self): | ||||
self.assertEqual(0, len(core.get_env())) | 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())) | self.assertEqual(0, len(core.get_env())) | ||||
os.environ['KOSMORRO_GREAT_VARIABLE'] = 'value' | |||||
os.environ["KOSMORRO_GREAT_VARIABLE"] = "value" | |||||
env = core.get_env() | env = core.get_env() | ||||
self.assertEqual(1, len(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() | env = core.get_env() | ||||
self.assertEqual(2, len(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): | 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)) | self.assertEqual(core.get_date("1111-11-13"), date(1111, 11, 13)) | ||||
if __name__ == '__main__': | |||||
if __name__ == "__main__": | |||||
unittest.main() | unittest.main() |
@@ -5,13 +5,15 @@ from kosmorrolib import data, core | |||||
class DataTestCase(unittest.TestCase): | class DataTestCase(unittest.TestCase): | ||||
def test_object_radius_must_be_set_to_get_apparent_radius(self): | 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: | 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() | unittest.main() |
@@ -12,13 +12,17 @@ class DateUtilTestCase(unittest.TestCase): | |||||
date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 0), to_tz=2) | date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 0), to_tz=2) | ||||
self.assertEqual(2, date.hour) | 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) | 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(8, date.day) | ||||
self.assertEqual(23, date.hour) | self.assertEqual(23, date.hour) | ||||
if __name__ == '__main__': | |||||
if __name__ == "__main__": | |||||
unittest.main() | unittest.main() |
@@ -12,16 +12,17 @@ from kosmorrolib.exceptions import OutOfRangeDateError | |||||
class EphemeridesTestCase(unittest.TestCase): | class EphemeridesTestCase(unittest.TestCase): | ||||
def test_get_ephemerides_for_aster_returns_correct_hours(self): | def test_get_ephemerides_for_aster_returns_correct_hours(self): | ||||
position = Position(0, 0, EARTH) | 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) | @expect_assertions(self.assertRegex, num=3) | ||||
def do_assertions(assert_regex): | def do_assertions(assert_regex): | ||||
for ephemeris in eph: | 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 | break | ||||
do_assertions() | do_assertions() | ||||
@@ -34,61 +35,61 @@ class EphemeridesTestCase(unittest.TestCase): | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 25)) | phase = ephemerides.get_moon_phase(date(2019, 11, 25)) | ||||
self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type) | self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type) | ||||
self.assertIsNone(phase.time) | 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)) | phase = ephemerides.get_moon_phase(date(2019, 11, 26)) | ||||
self.assertEqual(MoonPhaseType.NEW_MOON, phase.phase_type) | 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)) | phase = ephemerides.get_moon_phase(date(2019, 11, 27)) | ||||
self.assertEqual(MoonPhaseType.WAXING_CRESCENT, phase.phase_type) | self.assertEqual(MoonPhaseType.WAXING_CRESCENT, phase.phase_type) | ||||
self.assertIsNone(phase.time) | 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): | def test_moon_phase_first_crescent(self): | ||||
phase = ephemerides.get_moon_phase(date(2019, 11, 3)) | phase = ephemerides.get_moon_phase(date(2019, 11, 3)) | ||||
self.assertEqual(MoonPhaseType.WAXING_CRESCENT, phase.phase_type) | self.assertEqual(MoonPhaseType.WAXING_CRESCENT, phase.phase_type) | ||||
self.assertIsNone(phase.time) | 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)) | phase = ephemerides.get_moon_phase(date(2019, 11, 4)) | ||||
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.phase_type) | 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)) | phase = ephemerides.get_moon_phase(date(2019, 11, 5)) | ||||
self.assertEqual(MoonPhaseType.WAXING_GIBBOUS, phase.phase_type) | self.assertEqual(MoonPhaseType.WAXING_GIBBOUS, phase.phase_type) | ||||
self.assertIsNone(phase.time) | 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): | def test_moon_phase_full_moon(self): | ||||
phase = ephemerides.get_moon_phase(date(2019, 11, 11)) | phase = ephemerides.get_moon_phase(date(2019, 11, 11)) | ||||
self.assertEqual(MoonPhaseType.WAXING_GIBBOUS, phase.phase_type) | self.assertEqual(MoonPhaseType.WAXING_GIBBOUS, phase.phase_type) | ||||
self.assertIsNone(phase.time) | 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)) | phase = ephemerides.get_moon_phase(date(2019, 11, 12)) | ||||
self.assertEqual(MoonPhaseType.FULL_MOON, phase.phase_type) | 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)) | phase = ephemerides.get_moon_phase(date(2019, 11, 13)) | ||||
self.assertEqual(MoonPhaseType.WANING_GIBBOUS, phase.phase_type) | self.assertEqual(MoonPhaseType.WANING_GIBBOUS, phase.phase_type) | ||||
self.assertIsNone(phase.time) | 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): | def test_moon_phase_last_quarter(self): | ||||
phase = ephemerides.get_moon_phase(date(2019, 11, 18)) | phase = ephemerides.get_moon_phase(date(2019, 11, 18)) | ||||
self.assertEqual(MoonPhaseType.WANING_GIBBOUS, phase.phase_type) | self.assertEqual(MoonPhaseType.WANING_GIBBOUS, phase.phase_type) | ||||
self.assertIsNone(phase.time) | 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)) | phase = ephemerides.get_moon_phase(date(2019, 11, 19)) | ||||
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.phase_type) | 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)) | phase = ephemerides.get_moon_phase(date(2019, 11, 20)) | ||||
self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type) | self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type) | ||||
self.assertIsNone(phase.time) | 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): | def test_moon_phase_prediction(self): | ||||
phase = MoonPhase(MoonPhaseType.NEW_MOON, None, None) | phase = MoonPhase(MoonPhaseType.NEW_MOON, None, None) | ||||
@@ -120,5 +121,5 @@ class EphemeridesTestCase(unittest.TestCase): | |||||
ephemerides.get_moon_phase(date(1789, 5, 5)) | ephemerides.get_moon_phase(date(1789, 5, 5)) | ||||
if __name__ == '__main__': | |||||
if __name__ == "__main__": | |||||
unittest.main() | unittest.main() |
@@ -10,43 +10,111 @@ from kosmorrolib.exceptions import OutOfRangeDateError | |||||
EXPECTED_EVENTS = [ | EXPECTED_EVENTS = [ | ||||
(date(2020, 2, 7), []), | (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) | @parameterized.expand(EXPECTED_EVENTS) | ||||
def test_search_events(self, d: date, expected_events: [Event]): | def test_search_events(self, d: date, expected_events: [Event]): | ||||
actual_events = events.get_events(d) | 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): | for i, expected_event in enumerate(expected_events): | ||||
actual_event = actual_events[i] | actual_event = actual_events[i] | ||||
# Remove unnecessary precision (seconds and microseconds) | # 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__) | self.assertEqual(expected_event.__dict__, actual_event.__dict__) | ||||
@@ -79,5 +150,5 @@ class EventTestCase(unittest.TestCase): | |||||
events.get_events(date(1789, 5, 5)) | events.get_events(date(1789, 5, 5)) | ||||
if __name__ == '__main__': | |||||
if __name__ == "__main__": | |||||
unittest.main() | unittest.main() |
@@ -39,9 +39,10 @@ def expect_assertions(assert_fun, num=1): | |||||
count = assert_fun_mock.call_count | count = assert_fun_mock.call_count | ||||
if count != num: | 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 | return sniff_function | ||||