Browse Source

test: enhance the coverage (#31)

tags/v1.0.0
Jérôme Deuchnord 2 years ago
committed by Jérôme Deuchnord
parent
commit
0b72046c2d
No known key found for this signature in database GPG Key ID: 9F72B1EF93EDE1D4
15 changed files with 313 additions and 600 deletions
  1. +0
    -50
      .github/workflows/tests.yml
  2. +5
    -8
      Makefile
  3. +15
    -5
      kosmorrolib/dateutil.py
  4. +8
    -8
      kosmorrolib/enum.py
  5. +85
    -67
      kosmorrolib/ephemerides.py
  6. +75
    -39
      kosmorrolib/events.py
  7. +8
    -18
      kosmorrolib/exceptions.py
  8. +117
    -39
      kosmorrolib/model.py
  9. +0
    -16
      tests.py
  10. +0
    -23
      tests/__init__.py
  11. +0
    -33
      tests/core.py
  12. +0
    -38
      tests/data.py
  13. +0
    -46
      tests/dateutil.py
  14. +0
    -143
      tests/ephemerides.py
  15. +0
    -67
      tests/testutils.py

+ 0
- 50
.github/workflows/tests.yml View File

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


+ 5
- 8
Makefile View File

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


+ 15
- 5
kosmorrolib/dateutil.py View File

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


+ 8
- 8
kosmorrolib/enum.py View File

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


+ 85
- 67
kosmorrolib/ephemerides.py View File

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



+ 75
- 39
kosmorrolib/events.py View File

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


+ 8
- 18
kosmorrolib/exceptions.py View File

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

+ 117
- 39
kosmorrolib/model.py View File

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



+ 0
- 16
tests.py View File

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


+ 0
- 23
tests/__init__.py View File

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

+ 0
- 33
tests/core.py View File

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

+ 0
- 38
tests/data.py View File

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

+ 0
- 46
tests/dateutil.py View File

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

+ 0
- 143
tests/ephemerides.py View File

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

+ 0
- 67
tests/testutils.py View File

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

Loading…
Cancel
Save