Browse Source

ci(black): fix the coding standards

tags/v0.10.0
Jérôme Deuchnord 3 years ago
parent
commit
c956e52ce8
15 changed files with 467 additions and 236 deletions
  1. +3
    -0
      Makefile
  2. +8
    -8
      kosmorrolib/__version__.py
  3. +24
    -15
      kosmorrolib/core.py
  4. +89
    -58
      kosmorrolib/data.py
  5. +3
    -1
      kosmorrolib/dateutil.py
  6. +2
    -0
      kosmorrolib/enum.py
  7. +64
    -26
      kosmorrolib/ephemerides.py
  8. +92
    -38
      kosmorrolib/events.py
  9. +4
    -2
      kosmorrolib/exceptions.py
  10. +22
    -11
      tests/core.py
  11. +6
    -4
      tests/data.py
  12. +7
    -3
      tests/dateutil.py
  13. +20
    -19
      tests/ephemerides.py
  14. +119
    -48
      tests/events.py
  15. +4
    -3
      tests/testutils.py

+ 3
- 0
Makefile View File

@@ -1,3 +1,6 @@
black:
pipenv run black kosmorrolib tests

.PHONY: test
tests: legacy-tests
python3 tests.py


+ 8
- 8
kosmorrolib/__version__.py View File

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

+ 24
- 15
kosmorrolib/core.py View File

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

+ 89
- 58
kosmorrolib/data.py View File

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

+ 3
- 1
kosmorrolib/dateutil.py View File

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

+ 2
- 0
kosmorrolib/enum.py View File

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


+ 64
- 26
kosmorrolib/ephemerides.py View File

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


+ 92
- 38
kosmorrolib/events.py View File

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


+ 4
- 2
kosmorrolib/exceptions.py View File

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


+ 22
- 11
tests/core.py View File

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

+ 6
- 4
tests/data.py View File

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

+ 7
- 3
tests/dateutil.py View File

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

+ 20
- 19
tests/ephemerides.py View File

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

+ 119
- 48
tests/events.py View File

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

+ 4
- 3
tests/testutils.py View File

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



Loading…
Cancel
Save