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