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