@@ -7,56 +7,6 @@ on: | |||
branches: [main, features] | |||
jobs: | |||
legacy-tests: | |||
runs-on: ${{ matrix.os }} | |||
strategy: | |||
fail-fast: false | |||
matrix: | |||
os: | |||
- ubuntu-18.04 | |||
- ubuntu-20.04 | |||
- macos-10.15 | |||
- macos-11.0 | |||
- windows-2019 | |||
python_version: | |||
- '3.7' | |||
- '3.8' | |||
- '3.9' | |||
name: Legacy tests (Python ${{ matrix.python_version }} on ${{ matrix.os }}) | |||
steps: | |||
- uses: actions/checkout@v1 | |||
- name: Set up Python | |||
uses: actions/setup-python@v1 | |||
with: | |||
python-version: ${{ matrix.python_version }} | |||
- name: Prepare environment (non-Windows systems) | |||
if: ${{ matrix.os != 'windows-2019' }} | |||
run: | | |||
pip install --upgrade pip pipenv | |||
- name: Prepare environment (Windows) | |||
if: ${{ matrix.os == 'windows-2019' }} | |||
run: | | |||
python -mpip install --upgrade pip pipenv | |||
- name: Install dependencies (all systems) | |||
run: | | |||
pipenv lock --pre | |||
pipenv sync --dev | |||
pipenv install --dev | |||
- name: Install dependencies (Windows) | |||
if: ${{ matrix.os == 'windows-2019' }} | |||
run: | | |||
pipenv lock --dev -r > requirements.txt | |||
python -mpip install -r requirements.txt | |||
- name: Run legacy tests | |||
run: | | |||
make legacy-tests | |||
doc-tests: | |||
runs-on: ${{ matrix.os }} | |||
strategy: | |||
@@ -1,21 +1,18 @@ | |||
black: | |||
pipenv run black kosmorrolib tests setup.py | |||
pipenv run black kosmorrolib setup.py | |||
.PHONY: tests | |||
tests: legacy-tests doctests | |||
tests: doctests | |||
coverage-doctests: | |||
pipenv run python3 -m coverage run tests.py | |||
coverage-report: | |||
pipenv run python3 -m coverage report | |||
doctests: | |||
pipenv run python3 tests.py | |||
legacy-tests: | |||
unset KOSMORRO_LATITUDE; \ | |||
unset KOSMORRO_LONGITUDE; \ | |||
unset KOSMORRO_TIMEZONE; \ | |||
pipenv run python3 -m unittest tests | |||
.PHONY: build | |||
build: | |||
python3 setup.py sdist bdist_wheel | |||
@@ -19,11 +19,21 @@ | |||
from datetime import datetime, timezone, timedelta | |||
def translate_to_timezone(date: datetime, to_tz: int, from_tz: int = None): | |||
if from_tz is not None: | |||
source_tz = timezone(timedelta(hours=from_tz)) | |||
else: | |||
source_tz = timezone.utc | |||
def translate_to_timezone(date: datetime, to_tz: int): | |||
"""Convert a datetime from a timezone to another. | |||
>>> translate_to_timezone(datetime(2021, 6, 9, 5, 0, 0, tzinfo=timezone.utc), 2) | |||
datetime.datetime(2021, 6, 9, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))) | |||
>>> translate_to_timezone(datetime(2021, 6, 9, 5, 0, 0, tzinfo=timezone(timedelta(hours=1))), 2) | |||
datetime.datetime(2021, 6, 9, 6, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))) | |||
If the datetime has no timezone information, then it is interpreted as UTC: | |||
>>> translate_to_timezone(datetime(2021, 6, 9, 5, 0, 0), 2) | |||
datetime.datetime(2021, 6, 9, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))) | |||
""" | |||
source_tz = date.tzinfo if date.tzinfo is not None else timezone.utc | |||
return date.replace(tzinfo=source_tz).astimezone( | |||
tz=timezone(timedelta(hours=to_tz)) | |||
@@ -22,14 +22,14 @@ from enum import Enum | |||
class MoonPhaseType(Enum): | |||
"""An enumeration of moon phases.""" | |||
NEW_MOON = 1 | |||
WAXING_CRESCENT = 2 | |||
FIRST_QUARTER = 3 | |||
WAXING_GIBBOUS = 4 | |||
FULL_MOON = 5 | |||
WANING_GIBBOUS = 6 | |||
LAST_QUARTER = 7 | |||
WANING_CRESCENT = 8 | |||
NEW_MOON = 0 | |||
WAXING_CRESCENT = 1 | |||
FIRST_QUARTER = 2 | |||
WAXING_GIBBOUS = 3 | |||
FULL_MOON = 4 | |||
WANING_GIBBOUS = 5 | |||
LAST_QUARTER = 6 | |||
WANING_CRESCENT = 7 | |||
class SeasonType(Enum): | |||
@@ -16,7 +16,7 @@ | |||
# You should have received a copy of the GNU Affero General Public License | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
import datetime | |||
from datetime import date, datetime, timedelta | |||
from typing import Union | |||
from skyfield.searchlib import find_discrete, find_maxima | |||
@@ -40,60 +40,63 @@ def _get_skyfield_to_moon_phase( | |||
now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1 | |||
) | |||
phases = list(MoonPhaseType) | |||
current_phase = None | |||
current_phase_time = None | |||
next_phase_time = None | |||
i = 0 | |||
if len(times) == 0: | |||
return None | |||
# Find the current moon phase: | |||
for i, time in enumerate(times): | |||
if now.utc_iso() <= time.utc_iso(): | |||
if vals[i] in [0, 2, 4, 6]: | |||
if time.utc_datetime() < tomorrow.utc_datetime(): | |||
current_phase_time = time | |||
current_phase = phases[vals[i]] | |||
else: | |||
i -= 1 | |||
current_phase_time = None | |||
current_phase = phases[vals[i]] | |||
else: | |||
current_phase = phases[vals[i]] | |||
if now.utc_datetime() <= time.utc_datetime(): | |||
if time.utc_datetime() >= tomorrow.utc_datetime(): | |||
i -= 1 | |||
break | |||
current_phase = MoonPhaseType(vals[i]) | |||
if current_phase in [ | |||
MoonPhaseType.NEW_MOON, | |||
MoonPhaseType.FIRST_QUARTER, | |||
MoonPhaseType.FULL_MOON, | |||
MoonPhaseType.LAST_QUARTER, | |||
]: | |||
current_phase_time = translate_to_timezone(times[i].utc_datetime(), timezone) | |||
else: | |||
current_phase_time = None | |||
# Find the next moon phase | |||
for j in range(i + 1, len(times)): | |||
if vals[j] in [0, 2, 4, 6]: | |||
next_phase_time = times[j] | |||
next_phase_time = translate_to_timezone(times[j].utc_datetime(), timezone) | |||
break | |||
return MoonPhase( | |||
current_phase, | |||
translate_to_timezone(current_phase_time.utc_datetime(), timezone) | |||
if current_phase_time is not None | |||
else None, | |||
translate_to_timezone(next_phase_time.utc_datetime(), timezone) | |||
if next_phase_time is not None | |||
else None, | |||
) | |||
return MoonPhase(current_phase, current_phase_time, next_phase_time) | |||
def get_moon_phase( | |||
for_date: datetime.date = datetime.date.today(), timezone: int = 0 | |||
) -> MoonPhase: | |||
def get_moon_phase(for_date: date = date.today(), timezone: int = 0) -> MoonPhase: | |||
"""Calculate and return the moon phase for the given date, adjusted to the given timezone if any. | |||
Get the moon phase for the 27 March, 2021: | |||
>>> get_moon_phase(datetime.date.fromisoformat("2021-03-27")) | |||
>>> get_moon_phase(date(2021, 3, 27)) | |||
<MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 18:48:10.902298+00:00> | |||
When the moon phase is a new moon, a first quarter, a full moon or a last quarter, you get the exact time | |||
of its happening too: | |||
>>> get_moon_phase(datetime(2021, 3, 28)) | |||
<MoonPhase phase_type=MoonPhaseType.FULL_MOON time=2021-03-28 18:48:10.902298+00:00 next_phase_date=2021-04-04 10:02:27.393689+00:00> | |||
Get the moon phase for the 27 March, 2021, in the UTC+2 timezone: | |||
>>> get_moon_phase(datetime.date.fromisoformat("2021-03-27"), timezone=2) | |||
>>> get_moon_phase(date(2021, 3, 27), timezone=2) | |||
<MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 20:48:10.902298+02:00> | |||
Note that the moon phase can only be computed for a date range. | |||
Asking for the moon phase with an out of range date will result in an exception: | |||
>>> get_moon_phase(date(1000, 1, 1)) | |||
Traceback (most recent call last): | |||
... | |||
kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-08-09 and 2053-09-26 | |||
""" | |||
earth = get_skf_objects()["earth"] | |||
moon = get_skf_objects()["moon"] | |||
@@ -109,56 +112,69 @@ def get_moon_phase( | |||
moon_phase_at.rough_period = 7.0 # one lunar phase per week | |||
today = get_timescale().utc(for_date.year, for_date.month, for_date.day) | |||
time1 = get_timescale().utc(for_date.year, for_date.month, for_date.day - 10) | |||
time2 = get_timescale().utc(for_date.year, for_date.month, for_date.day + 10) | |||
start_time = get_timescale().utc(for_date.year, for_date.month, for_date.day - 10) | |||
end_time = get_timescale().utc(for_date.year, for_date.month, for_date.day + 10) | |||
try: | |||
times, phase = find_discrete(time1, time2, moon_phase_at) | |||
times, phases = find_discrete(start_time, end_time, moon_phase_at) | |||
return _get_skyfield_to_moon_phase(times, phases, today, timezone) | |||
except EphemerisRangeError as error: | |||
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 | |||
) | |||
end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12) | |||
start = date(start.year, start.month, start.day) + timedelta(days=12) | |||
end = date(end.year, end.month, end.day) - timedelta(days=12) | |||
raise OutOfRangeDateError(start, end) from error | |||
return _get_skyfield_to_moon_phase(times, phase, today, timezone) | |||
def get_ephemerides( | |||
position: Position, date: datetime.date = datetime.date.today(), timezone: int = 0 | |||
position: Position, for_date: date = date.today(), timezone: int = 0 | |||
) -> [AsterEphemerides]: | |||
"""Compute and return the ephemerides for the given position and date, adjusted to the given timezone if any. | |||
Compute the ephemerides for June 9th, 2021: | |||
>>> pos = Position(50.5824, 3.0624) | |||
>>> get_ephemerides(pos, datetime.date(2021, 6, 9)) | |||
>>> get_ephemerides(pos, date(2021, 6, 9)) | |||
[<AsterEphemerides rise_time=2021-06-09 03:36:00 culmination_time=2021-06-09 11:47:00 set_time=2021-06-09 19:58:00 aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-09 02:59:00 culmination_time=2021-06-09 11:02:00 set_time=2021-06-09 19:16:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-09 04:06:00 culmination_time=2021-06-09 11:58:00 set_time=2021-06-09 19:49:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-06-09 04:52:00 culmination_time=2021-06-09 13:13:00 set_time=2021-06-09 21:34:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-09 06:38:00 culmination_time=2021-06-09 14:40:00 set_time=2021-06-09 22:41:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-09 23:43:00 culmination_time=2021-06-09 04:54:00 set_time=2021-06-09 10:01:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-09 23:02:00 culmination_time=2021-06-09 03:41:00 set_time=2021-06-09 08:16:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-09 01:56:00 culmination_time=2021-06-09 09:18:00 set_time=2021-06-09 16:40:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-09 00:27:00 culmination_time=2021-06-09 06:13:00 set_time=2021-06-09 11:59:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=2021-06-09 22:22:00 culmination_time=2021-06-09 02:32:00 set_time=2021-06-09 06:38:00 aster=<Object type=PLANET name=PLUTO />>] | |||
Compute the ephemerides for June 9th, 2021: | |||
>>> get_ephemerides(pos, datetime.date(2021, 6, 9), timezone=2) | |||
>>> get_ephemerides(pos, date(2021, 6, 9), timezone=2) | |||
[<AsterEphemerides rise_time=2021-06-09 05:36:00 culmination_time=2021-06-09 13:47:00 set_time=2021-06-09 21:58:00 aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-09 04:59:00 culmination_time=2021-06-09 13:02:00 set_time=2021-06-09 21:16:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-09 06:06:00 culmination_time=2021-06-09 13:58:00 set_time=2021-06-09 21:49:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-06-09 06:52:00 culmination_time=2021-06-09 15:13:00 set_time=2021-06-09 23:34:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-09 08:38:00 culmination_time=2021-06-09 16:40:00 set_time=2021-06-09 00:44:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-09 01:47:00 culmination_time=2021-06-09 06:54:00 set_time=2021-06-09 12:01:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-09 01:06:00 culmination_time=2021-06-09 05:41:00 set_time=2021-06-09 10:16:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-09 03:56:00 culmination_time=2021-06-09 11:18:00 set_time=2021-06-09 18:40:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-09 02:27:00 culmination_time=2021-06-09 08:13:00 set_time=2021-06-09 13:59:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=2021-06-09 00:26:00 culmination_time=2021-06-09 04:32:00 set_time=2021-06-09 08:38:00 aster=<Object type=PLANET name=PLUTO />>] | |||
Objects may not rise or set on the given date (i.e. they rise the previous day or set the next day). | |||
When this happens, you will get `None` values on the rise or set time. | |||
Note that this is timezone-dependent: | |||
>>> get_ephemerides(Position(50.5876, 3.0624), date(2021, 9, 14), timezone=2)[1] | |||
<AsterEphemerides rise_time=2021-09-14 16:46:00 culmination_time=2021-09-14 20:29:00 set_time=None aster=<Object type=SATELLITE name=MOON />> | |||
If an objet does not rise nor set due to your latitude, then both rise and set will be `None`: | |||
>>> north_pole = Position(70, 20) | |||
>>> south_pole = Position(-70, 20) | |||
>>> get_ephemerides(north_pole, datetime.date(2021, 6, 20)) | |||
>>> get_ephemerides(north_pole, date(2021, 6, 20)) | |||
[<AsterEphemerides rise_time=None culmination_time=2021-06-20 10:42:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-20 14:30:00 culmination_time=2021-06-20 18:44:00 set_time=2021-06-20 22:53:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-20 22:56:00 culmination_time=2021-06-20 09:47:00 set_time=2021-06-20 20:34:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 12:20:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 13:17:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-20 23:06:00 culmination_time=2021-06-20 03:04:00 set_time=2021-06-20 06:58:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-20 23:28:00 culmination_time=2021-06-20 01:48:00 set_time=2021-06-20 04:05:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-20 21:53:00 culmination_time=2021-06-20 07:29:00 set_time=2021-06-20 17:02:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-20 22:51:00 culmination_time=2021-06-20 04:22:00 set_time=2021-06-20 09:50:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 00:40:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | |||
>>> get_ephemerides(north_pole, datetime.date(2021, 12, 21)) | |||
>>> get_ephemerides(north_pole, date(2021, 12, 21)) | |||
[<AsterEphemerides rise_time=None culmination_time=2021-12-21 10:38:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 00:04:00 set_time=None aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 11:33:00 set_time=None aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-12-21 11:58:00 culmination_time=2021-12-21 12:33:00 set_time=2021-12-21 13:08:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 08:54:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-12-21 11:07:00 culmination_time=2021-12-21 14:43:00 set_time=2021-12-21 18:19:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-12-21 11:32:00 culmination_time=2021-12-21 13:33:00 set_time=2021-12-21 15:33:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-12-21 09:54:00 culmination_time=2021-12-21 19:13:00 set_time=2021-12-21 04:37:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-12-21 10:49:00 culmination_time=2021-12-21 16:05:00 set_time=2021-12-21 21:21:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 12:31:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | |||
>>> get_ephemerides(south_pole, datetime.date(2021, 6, 20)) | |||
>>> get_ephemerides(south_pole, date(2021, 6, 20)) | |||
[<AsterEphemerides rise_time=None culmination_time=2021-06-20 10:42:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-20 11:10:00 culmination_time=2021-06-20 19:06:00 set_time=2021-06-20 01:20:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-20 07:47:00 culmination_time=2021-06-20 09:47:00 set_time=2021-06-20 11:48:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 12:20:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-20 12:14:00 culmination_time=2021-06-20 13:17:00 set_time=2021-06-20 14:21:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-20 18:32:00 culmination_time=2021-06-20 03:04:00 set_time=2021-06-20 11:32:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-20 15:20:00 culmination_time=2021-06-20 01:48:00 set_time=2021-06-20 12:12:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-20 04:32:00 culmination_time=2021-06-20 07:29:00 set_time=2021-06-20 10:26:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-20 21:28:00 culmination_time=2021-06-20 04:22:00 set_time=2021-06-20 11:13:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 00:40:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | |||
>>> get_ephemerides(south_pole, datetime.date(2021, 12, 22)) | |||
>>> get_ephemerides(south_pole, date(2021, 12, 22)) | |||
[<AsterEphemerides rise_time=None culmination_time=2021-12-22 10:39:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 01:01:00 set_time=None aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 11:35:00 set_time=None aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 12:27:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 08:53:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-12-22 05:52:00 culmination_time=2021-12-22 14:40:00 set_time=2021-12-22 23:26:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-12-22 02:41:00 culmination_time=2021-12-22 13:29:00 set_time=2021-12-22 00:21:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-12-22 16:01:00 culmination_time=2021-12-22 19:09:00 set_time=2021-12-22 22:17:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-12-22 08:59:00 culmination_time=2021-12-22 16:01:00 set_time=2021-12-22 23:04:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 12:27:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | |||
Note that the ephemerides can only be computed for a date range. | |||
Asking for the ephemerides with an out of range date will result in an exception: | |||
>>> get_ephemerides(pos, date(1000, 1, 1)) | |||
Traceback (most recent call last): | |||
... | |||
kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-07-29 and 2053-10-07 | |||
""" | |||
ephemerides = [] | |||
@@ -167,7 +183,7 @@ def get_ephemerides( | |||
return ( | |||
position.get_planet_topos() | |||
.at(time) | |||
.observe(for_aster.get_skyfield_object()) | |||
.observe(for_aster.skyfield_object) | |||
.apparent() | |||
.altaz()[0] | |||
.degrees | |||
@@ -183,26 +199,28 @@ def get_ephemerides( | |||
fun.rough_period = 0.5 | |||
return fun | |||
start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) | |||
start_time = get_timescale().utc( | |||
for_date.year, for_date.month, for_date.day, -timezone | |||
) | |||
end_time = get_timescale().utc( | |||
date.year, date.month, date.day, 23 - timezone, 59, 59 | |||
for_date.year, for_date.month, for_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.0 / 3600 / 24, | |||
num=12, | |||
) | |||
culmination_time = ( | |||
culmination_time[0] if len(culmination_time) > 0 else None | |||
) | |||
except ValueError: | |||
culmination_time, _ = find_maxima( | |||
start_time, | |||
end_time, | |||
f=get_angle(aster), | |||
epsilon=1.0 / 3600 / 24, | |||
num=12, | |||
) | |||
if len(culmination_time) == 1: | |||
culmination_time = culmination_time[0] | |||
else: | |||
culmination_time = None | |||
rise_time, set_time = None, None | |||
@@ -244,8 +262,8 @@ def get_ephemerides( | |||
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 + 1) | |||
end = datetime.date(end.year, end.month, end.day - 1) | |||
start = date(start.year, start.month, start.day + 1) | |||
end = date(end.year, end.month, end.day - 1) | |||
raise OutOfRangeDateError(start, end) from error | |||
@@ -25,21 +25,32 @@ from skyfield.units import Angle | |||
from skyfield import almanac, eclipselib | |||
from numpy import pi | |||
from kosmorrolib.model import Object, Event, Star, Planet, ASTERS, EARTH | |||
from kosmorrolib.model import ( | |||
Object, | |||
Event, | |||
Object, | |||
Star, | |||
Planet, | |||
get_aster, | |||
ASTERS, | |||
EARTH, | |||
) | |||
from kosmorrolib.dateutil import translate_to_timezone | |||
from kosmorrolib.enum import EventType, ObjectIdentifier, SeasonType, LunarEclipseType | |||
from kosmorrolib.exceptions import OutOfRangeDateError | |||
from kosmorrolib.core import get_timescale, get_skf_objects, flatten_list | |||
def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Event]: | |||
def _search_conjunctions_occultations( | |||
start_time: Time, end_time: Time, timezone: int | |||
) -> [Event]: | |||
"""Function to search conjunction. | |||
**Warning:** this is an internal function, not intended for use by end-developers. | |||
Will return MOON and VENUS opposition on 2021-06-12: | |||
>>> conjunction = _search_conjunction(get_timescale().utc(2021,6,12),get_timescale().utc(2021,6,13),0) | |||
>>> conjunction = _search_conjunctions_occultations(get_timescale().utc(2021, 6, 12), get_timescale().utc(2021, 6, 13), 0) | |||
>>> len(conjunction) | |||
1 | |||
>>> conjunction[0].objects | |||
@@ -47,8 +58,13 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
Will return nothing if no conjunction happens: | |||
>>> _search_conjunction(get_timescale().utc(2021,6,17),get_timescale().utc(2021,6,18),0) | |||
>>> _search_conjunctions_occultations(get_timescale().utc(2021, 6, 17),get_timescale().utc(2021, 6, 18), 0) | |||
[] | |||
This function detects occultations too: | |||
>>> _search_conjunctions_occultations(get_timescale().utc(2021, 4, 17),get_timescale().utc(2021, 4, 18), 0) | |||
[<Event type=OCCULTATION objects=[<Object type=SATELLITE name=MOON />, <Object type=PLANET name=MARS />] start=2021-04-17 12:08:16.115650+00:00 end=None details=None />] | |||
""" | |||
earth = get_skf_objects()["earth"] | |||
aster1 = None | |||
@@ -57,10 +73,10 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
def is_in_conjunction(time: Time): | |||
earth_pos = earth.at(time) | |||
_, aster1_lon, _ = ( | |||
earth_pos.observe(aster1.get_skyfield_object()).apparent().ecliptic_latlon() | |||
earth_pos.observe(aster1.skyfield_object).apparent().ecliptic_latlon() | |||
) | |||
_, aster2_lon, _ = ( | |||
earth_pos.observe(aster2.get_skyfield_object()).apparent().ecliptic_latlon() | |||
earth_pos.observe(aster2.skyfield_object).apparent().ecliptic_latlon() | |||
) | |||
return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype( | |||
@@ -70,7 +86,7 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
is_in_conjunction.rough_period = 60.0 | |||
computed = [] | |||
conjunctions = [] | |||
events = [] | |||
for aster1 in ASTERS: | |||
# Ignore the Sun | |||
@@ -85,20 +101,21 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
for i, time in enumerate(times): | |||
if is_conjs[i]: | |||
aster1_pos = (aster1.get_skyfield_object() - earth).at(time) | |||
aster2_pos = (aster2.get_skyfield_object() - earth).at(time) | |||
aster1_pos = (aster1.skyfield_object - earth).at(time) | |||
aster2_pos = (aster2.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): | |||
if ( | |||
distance - aster2.get_apparent_radius(time).degrees | |||
< aster1.get_apparent_radius(time).degrees | |||
): | |||
occulting_aster = ( | |||
[aster1, aster2] | |||
if aster1_pos.distance().km < aster2_pos.distance().km | |||
else [aster2, aster1] | |||
) | |||
conjunctions.append( | |||
events.append( | |||
Event( | |||
EventType.OCCULTATION, | |||
occulting_aster, | |||
@@ -106,7 +123,7 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
) | |||
) | |||
else: | |||
conjunctions.append( | |||
events.append( | |||
Event( | |||
EventType.CONJUNCTION, | |||
[aster1, aster2], | |||
@@ -116,7 +133,7 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
computed.append(aster1) | |||
return conjunctions | |||
return events | |||
def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Event]: | |||
@@ -151,7 +168,7 @@ def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
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(aster.skyfield_object).apparent() | |||
_, lon1, _ = sun_pos.ecliptic_latlon() | |||
_, lon2, _ = aster_pos.ecliptic_latlon() | |||
@@ -189,26 +206,39 @@ def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||
def _search_maximal_elongations( | |||
start_time: Time, end_time: Time, timezone: int | |||
) -> [Event]: | |||
"""Function to search oppositions. | |||
**Warning:** this is an internal function, not intended for use by end-developers. | |||
Will return Mercury maimum elogation for September 14, 2021: | |||
>>> get_events(date(2021, 9, 14)) | |||
[<Event type=MAXIMAL_ELONGATION objects=[<Object type=PLANET name=MERCURY />] start=2021-09-14 04:13:46.664879+00:00 end=None details={'deg': 26.8} />] | |||
""" | |||
earth = get_skf_objects()["earth"] | |||
sun = get_skf_objects()["sun"] | |||
aster = None | |||
def get_elongation(time: Time): | |||
sun_pos = (sun - earth).at(time) | |||
aster_pos = (aster.get_skyfield_object() - earth).at(time) | |||
separation = sun_pos.separation_from(aster_pos) | |||
return separation.degrees | |||
def get_elongation(planet: Object): | |||
def f(time: Time): | |||
sun_pos = (sun - earth).at(time) | |||
aster_pos = (planet.skyfield_object - earth).at(time) | |||
separation = sun_pos.separation_from(aster_pos) | |||
return separation.degrees | |||
get_elongation.rough_period = 1.0 | |||
f.rough_period = 1.0 | |||
events = [] | |||
return f | |||
for aster in ASTERS: | |||
if aster.skyfield_name not in ["MERCURY", "VENUS"]: | |||
continue | |||
events = [] | |||
for identifier in [ObjectIdentifier.MERCURY, ObjectIdentifier.VENUS]: | |||
planet = get_aster(identifier) | |||
times, elongations = find_maxima( | |||
start_time, end_time, f=get_elongation, epsilon=1.0 / 24 / 3600, num=12 | |||
start_time, | |||
end_time, | |||
f=get_elongation(planet), | |||
epsilon=1.0 / 24 / 3600, | |||
num=12, | |||
) | |||
for i, time in enumerate(times): | |||
@@ -216,7 +246,7 @@ def _search_maximal_elongations( | |||
events.append( | |||
Event( | |||
EventType.MAXIMAL_ELONGATION, | |||
[aster], | |||
[planet], | |||
translate_to_timezone(time.utc_datetime(), timezone), | |||
details={"deg": elongation}, | |||
) | |||
@@ -227,8 +257,8 @@ def _search_maximal_elongations( | |||
def _get_distance(to_aster: Object, from_aster: Object): | |||
def get_distance(time: Time): | |||
from_pos = from_aster.get_skyfield_object().at(time) | |||
to_pos = from_pos.observe(to_aster.get_skyfield_object()) | |||
from_pos = from_aster.skyfield_object.at(time) | |||
to_pos = from_pos.observe(to_aster.skyfield_object) | |||
return to_pos.distance().km | |||
@@ -370,25 +400,23 @@ def _search_lunar_eclipse(start_time: Time, end_time: Time, timezone: int) -> [E | |||
>>> _search_lunar_eclipse(get_timescale().utc(2017, 2, 11), get_timescale().utc(2017, 2, 12), 0) | |||
[<Event type=LUNAR_ECLIPSE objects=[<Object type=SATELLITE name=MOON />] start=2017-02-10 22:02:59.016572+00:00 end=2017-02-11 03:25:07.627886+00:00 details={'type': <LunarEclipseType.PENUMBRAL: 0>, 'maximum': datetime.datetime(2017, 2, 11, 0, 43, 51, 793786, tzinfo=datetime.timezone.utc)} />] | |||
""" | |||
moon = ASTERS[1] | |||
moon = get_aster(ObjectIdentifier.MOON) | |||
events = [] | |||
t, y, details = eclipselib.lunar_eclipses(start_time, end_time, get_skf_objects()) | |||
for ti, yi in zip(t, y): | |||
penumbra_radius = Angle(radians=details["penumbra_radius_radians"][0]) | |||
_, max_lon, _ = ( | |||
EARTH.get_skyfield_object() | |||
.at(ti) | |||
.observe(moon.get_skyfield_object()) | |||
EARTH.skyfield_object.at(ti) | |||
.observe(moon.skyfield_object) | |||
.apparent() | |||
.ecliptic_latlon() | |||
) | |||
def is_in_penumbra(time: Time): | |||
_, lon, _ = ( | |||
EARTH.get_skyfield_object() | |||
.at(time) | |||
.observe(moon.get_skyfield_object()) | |||
EARTH.skyfield_object.at(time) | |||
.observe(moon.skyfield_object) | |||
.apparent() | |||
.ecliptic_latlon() | |||
) | |||
@@ -453,6 +481,14 @@ def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]: | |||
>>> get_events(date(2021, 4, 20)) | |||
[] | |||
Note that the events can only be found for a date range. | |||
Asking for the events with an out of range date will result in an exception: | |||
>>> get_events(date(1000, 1, 1)) | |||
Traceback (most recent call last): | |||
... | |||
kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-07-28 and 2053-10-08 | |||
:param for_date: the date for which the events must be calculated | |||
:param timezone: the timezone to adapt the results to. If not given, defaults to 0. | |||
:return: a list of events found for the given date. | |||
@@ -470,7 +506,7 @@ def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]: | |||
for fun in [ | |||
_search_oppositions, | |||
_search_conjunction, | |||
_search_conjunctions_occultations, | |||
_search_maximal_elongations, | |||
_search_apogee(ASTERS[1]), | |||
_search_perigee(ASTERS[1]), | |||
@@ -19,24 +19,14 @@ | |||
from datetime import date | |||
class UnavailableFeatureError(RuntimeError): | |||
def __init__(self, msg: str): | |||
super().__init__() | |||
self.msg = msg | |||
class OutOfRangeDateError(RuntimeError): | |||
class OutOfRangeDateError(ValueError): | |||
def __init__(self, min_date: date, max_date: date): | |||
super().__init__() | |||
super().__init__( | |||
"The date must be between %s and %s" | |||
% ( | |||
min_date.strftime("%Y-%m-%d"), | |||
max_date.strftime("%Y-%m-%d"), | |||
) | |||
) | |||
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"), | |||
) | |||
class CompileError(RuntimeError): | |||
def __init__(self, msg): | |||
super().__init__() | |||
self.msg = msg |
@@ -18,14 +18,14 @@ | |||
from abc import ABC, abstractmethod | |||
from typing import Union | |||
from datetime import datetime | |||
from datetime import datetime, timezone | |||
from numpy import pi, arcsin | |||
import numpy | |||
from skyfield.api import Topos, Time | |||
from skyfield.api import Topos, Time, Angle | |||
from skyfield.vectorlib import VectorSum as SkfPlanet | |||
from .core import get_skf_objects | |||
from .core import get_skf_objects, get_timescale | |||
from .enum import MoonPhaseType, EventType, ObjectIdentifier, ObjectType | |||
@@ -54,6 +54,48 @@ class MoonPhase(Serializable): | |||
) | |||
def get_next_phase(self): | |||
"""Helper to get the Moon phase that follows the one described by the object. | |||
If the current Moon phase is New Moon or Waxing crescent, the next one will be First Quarter: | |||
>>> moon_phase = MoonPhase(MoonPhaseType.NEW_MOON) | |||
>>> moon_phase.get_next_phase() | |||
<MoonPhaseType.FIRST_QUARTER: 2> | |||
>>> moon_phase = MoonPhase(MoonPhaseType.NEW_MOON) | |||
>>> moon_phase.get_next_phase() | |||
<MoonPhaseType.FIRST_QUARTER: 2> | |||
If the current Moon phase is First Quarter or Waxing gibbous, the next one will be Full Moon: | |||
>>> moon_phase = MoonPhase(MoonPhaseType.FIRST_QUARTER) | |||
>>> moon_phase.get_next_phase() | |||
<MoonPhaseType.FULL_MOON: 4> | |||
>>> moon_phase = MoonPhase(MoonPhaseType.WAXING_GIBBOUS) | |||
>>> moon_phase.get_next_phase() | |||
<MoonPhaseType.FULL_MOON: 4> | |||
If the current Moon phase is Full Moon or Waning gibbous, the next one will be Last Quarter: | |||
>>> moon_phase = MoonPhase(MoonPhaseType.FULL_MOON) | |||
>>> moon_phase.get_next_phase() | |||
<MoonPhaseType.LAST_QUARTER: 6> | |||
>>> moon_phase = MoonPhase(MoonPhaseType.WANING_GIBBOUS) | |||
>>> moon_phase.get_next_phase() | |||
<MoonPhaseType.LAST_QUARTER: 6> | |||
If the current Moon phase is Last Quarter Moon or Waning crescent, the next one will be New Moon: | |||
>>> moon_phase = MoonPhase(MoonPhaseType.LAST_QUARTER) | |||
>>> moon_phase.get_next_phase() | |||
<MoonPhaseType.NEW_MOON: 0> | |||
>>> moon_phase = MoonPhase(MoonPhaseType.WANING_CRESCENT) | |||
>>> moon_phase.get_next_phase() | |||
<MoonPhaseType.NEW_MOON: 0> | |||
""" | |||
if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]: | |||
return MoonPhaseType.FIRST_QUARTER | |||
if self.phase_type in [ | |||
@@ -83,18 +125,20 @@ class Object(Serializable): | |||
""" | |||
def __init__( | |||
self, identifier: ObjectIdentifier, skyfield_name: str, radius: float = None | |||
self, | |||
identifier: ObjectIdentifier, | |||
skyfield_object: SkfPlanet, | |||
radius: float = None, | |||
): | |||
""" | |||
Initialize an astronomical object | |||
:param ObjectIdentifier identifier: the official name of the object (may be internationalized) | |||
:param str skyfield_name: the internal name of the object in Skyfield library | |||
:param str skyfield_object: the object from Skyfield library | |||
:param float radius: the radius (in km) of the object | |||
:param AsterEphemerides ephemerides: the ephemerides associated to the object | |||
""" | |||
self.identifier = identifier | |||
self.skyfield_name = skyfield_name | |||
self.skyfield_object = skyfield_object | |||
self.radius = radius | |||
def __repr__(self): | |||
@@ -103,32 +147,41 @@ class Object(Serializable): | |||
self.identifier.name, | |||
) | |||
def get_skyfield_object(self) -> SkfPlanet: | |||
return get_skf_objects()[self.skyfield_name] | |||
@abstractmethod | |||
def get_type(self) -> ObjectType: | |||
pass | |||
def get_apparent_radius(self, time: Time, from_place) -> float: | |||
""" | |||
Calculate the apparent radius, in degrees, of the object from the given place at a given time. | |||
:param time: | |||
:param from_place: | |||
:return: | |||
""" | |||
if self.radius is None: | |||
raise ValueError("Missing radius for %s" % self.identifier.name) | |||
def get_apparent_radius(self, for_date: Union[Time, datetime]) -> Angle: | |||
"""Calculate the apparent radius, in degrees, of the object from the given place at a given time. | |||
return ( | |||
360 | |||
/ pi | |||
* arcsin( | |||
self.radius | |||
/ from_place.at(time).observe(self.get_skyfield_object()).distance().km | |||
) | |||
**Warning:** this is an internal function, not intended for use by end-developers. | |||
For an easier usage, this method accepts datetime and Skyfield's Time objects: | |||
>>> sun = ASTERS[0] | |||
>>> sun.get_apparent_radius(datetime(2021, 6, 9, tzinfo=timezone.utc)) | |||
<Angle 00deg 31' 31.6"> | |||
>>> sun.get_apparent_radius(get_timescale().utc(2021, 6, 9)) | |||
<Angle 00deg 31' 31.6"> | |||
Source of the algorithm: https://rhodesmill.org/skyfield/examples.html#what-is-the-angular-diameter-of-a-planet-given-its-radius | |||
:param for_date: the date for which the apparent radius has to be returned | |||
:return: an object representing a Skyfield angle | |||
""" | |||
if isinstance(for_date, datetime): | |||
for_date = get_timescale().from_datetime(for_date) | |||
ra, dec, distance = ( | |||
EARTH.skyfield_object.at(for_date) | |||
.observe(self.skyfield_object) | |||
.apparent() | |||
.radec() | |||
) | |||
return Angle(radians=numpy.arcsin(self.radius / distance.km) * 2.0) | |||
def serialize(self) -> dict: | |||
"""Serialize the given object | |||
@@ -243,22 +296,47 @@ class AsterEphemerides(Serializable): | |||
} | |||
EARTH = Planet(ObjectIdentifier.EARTH, "EARTH") | |||
EARTH = Planet(ObjectIdentifier.EARTH, get_skf_objects()["EARTH"]) | |||
ASTERS = [ | |||
Star(ObjectIdentifier.SUN, "SUN", radius=696342), | |||
Satellite(ObjectIdentifier.MOON, "MOON", radius=1737.4), | |||
Planet(ObjectIdentifier.MERCURY, "MERCURY", radius=2439.7), | |||
Planet(ObjectIdentifier.VENUS, "VENUS", radius=6051.8), | |||
Planet(ObjectIdentifier.MARS, "MARS", radius=3396.2), | |||
Planet(ObjectIdentifier.JUPITER, "JUPITER BARYCENTER", radius=71492), | |||
Planet(ObjectIdentifier.SATURN, "SATURN BARYCENTER", radius=60268), | |||
Planet(ObjectIdentifier.URANUS, "URANUS BARYCENTER", radius=25559), | |||
Planet(ObjectIdentifier.NEPTUNE, "NEPTUNE BARYCENTER", radius=24764), | |||
Planet(ObjectIdentifier.PLUTO, "PLUTO BARYCENTER", radius=1185), | |||
Star(ObjectIdentifier.SUN, get_skf_objects()["SUN"], radius=696342), | |||
Satellite(ObjectIdentifier.MOON, get_skf_objects()["MOON"], radius=1737.4), | |||
Planet(ObjectIdentifier.MERCURY, get_skf_objects()["MERCURY"], radius=2439.7), | |||
Planet(ObjectIdentifier.VENUS, get_skf_objects()["VENUS"], radius=6051.8), | |||
Planet(ObjectIdentifier.MARS, get_skf_objects()["MARS"], radius=3396.2), | |||
Planet( | |||
ObjectIdentifier.JUPITER, get_skf_objects()["JUPITER BARYCENTER"], radius=71492 | |||
), | |||
Planet( | |||
ObjectIdentifier.SATURN, get_skf_objects()["SATURN BARYCENTER"], radius=60268 | |||
), | |||
Planet( | |||
ObjectIdentifier.URANUS, get_skf_objects()["URANUS BARYCENTER"], radius=25559 | |||
), | |||
Planet( | |||
ObjectIdentifier.NEPTUNE, get_skf_objects()["NEPTUNE BARYCENTER"], radius=24764 | |||
), | |||
Planet(ObjectIdentifier.PLUTO, get_skf_objects()["PLUTO BARYCENTER"], radius=1185), | |||
] | |||
def get_aster(identifier: ObjectIdentifier) -> Object: | |||
"""Return the aster with the given identifier | |||
>>> get_aster(ObjectIdentifier.SATURN) | |||
<Object type=PLANET name=SATURN /> | |||
You can also use it to get the `EARTH` object, even though it has its own constant: | |||
<Object type=PLANET name=EARTH /> | |||
""" | |||
if identifier == ObjectIdentifier.EARTH: | |||
return EARTH | |||
for aster in ASTERS: | |||
if aster.identifier == identifier: | |||
return aster | |||
class Position: | |||
def __init__(self, latitude: float, longitude: float): | |||
self.latitude = latitude | |||
@@ -267,7 +345,7 @@ class Position: | |||
def get_planet_topos(self) -> Topos: | |||
if self._topos is None: | |||
self._topos = EARTH.get_skyfield_object() + Topos( | |||
self._topos = EARTH.skyfield_object + Topos( | |||
latitude_degrees=self.latitude, longitude_degrees=self.longitude | |||
) | |||
@@ -1,21 +1,5 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU Affero General Public License as | |||
# published by the Free Software Foundation, either version 3 of the | |||
# License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU Affero General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU Affero General Public License | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
import doctest | |||
from kosmorrolib import * | |||
@@ -1,23 +0,0 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU Affero General Public License as | |||
# published by the Free Software Foundation, either version 3 of the | |||
# License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU Affero General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU Affero General Public License | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
from .core import * | |||
from .data import * | |||
from .ephemerides import * | |||
from .testutils import * | |||
from .dateutil import * |
@@ -1,33 +0,0 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU Affero General Public License as | |||
# published by the Free Software Foundation, either version 3 of the | |||
# License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU Affero General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU Affero General Public License | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
import unittest | |||
import kosmorrolib.core as core | |||
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]]), | |||
) | |||
if __name__ == "__main__": | |||
unittest.main() |
@@ -1,38 +0,0 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU Affero General Public License as | |||
# published by the Free Software Foundation, either version 3 of the | |||
# License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU Affero General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU Affero General Public License | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
import unittest | |||
from kosmorrolib import model, core | |||
from kosmorrolib.enum import ObjectIdentifier | |||
class DataTestCase(unittest.TestCase): | |||
def test_object_radius_must_be_set_to_get_apparent_radius(self): | |||
o = model.Planet(ObjectIdentifier.SATURN, "SATURN") | |||
with self.assertRaises(ValueError) as context: | |||
o.get_apparent_radius( | |||
core.get_timescale().now(), core.get_skf_objects()["earth"] | |||
) | |||
self.assertEqual(("Missing radius for SATURN",), context.exception.args) | |||
if __name__ == "__main__": | |||
unittest.main() |
@@ -1,46 +0,0 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU Affero General Public License as | |||
# published by the Free Software Foundation, either version 3 of the | |||
# License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU Affero General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU Affero General Public License | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
import unittest | |||
from kosmorrolib import dateutil | |||
from datetime import datetime | |||
class DateUtilTestCase(unittest.TestCase): | |||
def test_translate_to_timezone(self): | |||
date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 4), to_tz=-2) | |||
self.assertEqual(2, date.hour) | |||
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 | |||
) | |||
self.assertEqual(4, date.hour) | |||
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__": | |||
unittest.main() |
@@ -1,143 +0,0 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU Affero General Public License as | |||
# published by the Free Software Foundation, either version 3 of the | |||
# License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU Affero General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU Affero General Public License | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
import unittest | |||
from kosmorrolib.enum import MoonPhaseType | |||
from .testutils import expect_assertions | |||
from kosmorrolib import ephemerides | |||
from kosmorrolib.model import Position, MoonPhase | |||
from datetime import date | |||
from kosmorrolib.exceptions import OutOfRangeDateError | |||
class EphemeridesTestCase(unittest.TestCase): | |||
def test_get_ephemerides_for_aster_returns_correct_hours(self): | |||
position = Position(0, 0) | |||
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:42:") | |||
assert_regex( | |||
ephemeris.culmination_time.isoformat(), "^2019-11-18T11:45:" | |||
) | |||
assert_regex(ephemeris.set_time.isoformat(), "^2019-11-18T17:49:") | |||
break | |||
do_assertions() | |||
################################################################################################################### | |||
### MOON PHASE TESTS ### | |||
################################################################################################################### | |||
def test_moon_phase_new_moon(self): | |||
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") | |||
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") | |||
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") | |||
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") | |||
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") | |||
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") | |||
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") | |||
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") | |||
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") | |||
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") | |||
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") | |||
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") | |||
def test_moon_phase_prediction(self): | |||
phase = MoonPhase(MoonPhaseType.NEW_MOON, None, None) | |||
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.get_next_phase()) | |||
phase = MoonPhase(MoonPhaseType.WAXING_CRESCENT, None, None) | |||
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.get_next_phase()) | |||
phase = MoonPhase(MoonPhaseType.FIRST_QUARTER, None, None) | |||
self.assertEqual(MoonPhaseType.FULL_MOON, phase.get_next_phase()) | |||
phase = MoonPhase(MoonPhaseType.WAXING_GIBBOUS, None, None) | |||
self.assertEqual(MoonPhaseType.FULL_MOON, phase.get_next_phase()) | |||
phase = MoonPhase(MoonPhaseType.FULL_MOON, None, None) | |||
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.get_next_phase()) | |||
phase = MoonPhase(MoonPhaseType.WANING_GIBBOUS, None, None) | |||
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.get_next_phase()) | |||
phase = MoonPhase(MoonPhaseType.LAST_QUARTER, None, None) | |||
self.assertEqual(MoonPhaseType.NEW_MOON, phase.get_next_phase()) | |||
phase = MoonPhase(MoonPhaseType.WANING_CRESCENT, None, None) | |||
self.assertEqual(MoonPhaseType.NEW_MOON, phase.get_next_phase()) | |||
def test_get_ephemerides_raises_exception_on_out_of_date_range(self): | |||
with self.assertRaises(OutOfRangeDateError): | |||
ephemerides.get_ephemerides(Position(0, 0), date(1789, 5, 5)) | |||
def test_get_moon_phase_raises_exception_on_out_of_date_range(self): | |||
with self.assertRaises(OutOfRangeDateError): | |||
ephemerides.get_moon_phase(date(1789, 5, 5)) | |||
if __name__ == "__main__": | |||
unittest.main() |
@@ -1,67 +0,0 @@ | |||
#!/usr/bin/env python3 | |||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||
# | |||
# This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU Affero General Public License as | |||
# published by the Free Software Foundation, either version 3 of the | |||
# License, or (at your option) any later version. | |||
# | |||
# This program is distributed in the hope that it will be useful, | |||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
# GNU Affero General Public License for more details. | |||
# | |||
# You should have received a copy of the GNU Affero General Public License | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
import functools | |||
from unittest import mock | |||
def expect_assertions(assert_fun, num=1): | |||
"""Asserts that an assertion function is called as expected. | |||
This is very useful when the assertions are in loops. | |||
To use it, create a nested function in the the tests function. | |||
The nested function will receive as parameter the mocked assertion function to use in place of the original one. | |||
Finally, run the nested function. | |||
Example of use: | |||
>>> # the function we tests: | |||
>>> def my_sum_function(n, m): | |||
>>> # some code here | |||
>>> pass | |||
>>> # The unit tests: | |||
>>> def test_sum(self): | |||
>>> @expect_assertions(self.assertEqual, num=10): | |||
>>> def make_assertions(assert_equal): | |||
>>> for i in range (-5, 5): | |||
>>> for j in range(-5, 5): | |||
>>> assert_equal(i + j, my_sum_function(i, j) | |||
>>> | |||
>>> make_assertions() # You don't need to give any parameter, the decorator does it for you. | |||
:param assert_fun: the assertion function to tests | |||
:param num: the number of times the assertion function is expected to be called | |||
""" | |||
assert_fun_mock = mock.Mock(side_effect=assert_fun) | |||
def fun_decorator(fun): | |||
@functools.wraps(fun) | |||
def sniff_function(): | |||
fun(assert_fun_mock) | |||
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) | |||
) | |||
return sniff_function | |||
return fun_decorator |