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