@@ -4,7 +4,6 @@ on: [push] | |||
jobs: | |||
build: | |||
runs-on: ubuntu-latest | |||
steps: | |||
@@ -17,6 +16,9 @@ jobs: | |||
run: | | |||
pip install --upgrade pip pipenv | |||
pipenv sync -d | |||
- name: Unit tests | |||
run: | | |||
pipenv run python -m unittest -v test | |||
- name: Lint | |||
run: | | |||
pipenv run pylint kosmorro *.py kosmorrolib/*.py |
@@ -6,7 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
## [Unreleased] | |||
### Added | |||
- Update Numpy to v1.17.4 | |||
- Add JSON output (#6) | |||
### Changed | |||
### Removed | |||
## [0.1.0] - 2019-11-10 | |||
@@ -18,21 +18,13 @@ | |||
import argparse | |||
from datetime import date | |||
import numpy | |||
from kosmorrolib import dumper | |||
from kosmorrolib.ephemerides import EphemeridesComputer, Position | |||
# Fixes the "TypeError: Object of type int64 is not JSON serializable" | |||
# See https://stackoverflow.com/a/50577730 | |||
def json_default(obj): | |||
if isinstance(obj, numpy.int64): | |||
return int(obj) | |||
raise TypeError('Object of type ' + str(type(obj)) + ' could not be integrated in the JSON') | |||
def main(): | |||
args = get_args() | |||
output_formats = get_dumpers() | |||
args = get_args(list(output_formats.keys())) | |||
year = args.year | |||
month = args.month | |||
day = args.day | |||
@@ -43,11 +35,18 @@ def main(): | |||
ephemeris = EphemeridesComputer(Position(args.latitude, args.longitude, altitude=args.altitude)) | |||
ephemerides = ephemeris.compute_ephemerides(year, month, day) | |||
dump = dumper.TextDumper(ephemerides) | |||
dump = output_formats[args.format](ephemerides) | |||
print(dump.to_string()) | |||
def get_args(): | |||
def get_dumpers() -> {str: dumper.Dumper}: | |||
return { | |||
'text': dumper.TextDumper, | |||
'json': dumper.JsonDumper | |||
} | |||
def get_args(output_formats: [str]): | |||
today = date.today() | |||
parser = argparse.ArgumentParser(description='Compute the ephemerides for a given date, at a given position' | |||
@@ -56,6 +55,8 @@ def get_args(): | |||
' observer positioned at coordinates (0,0), with an altitude of 0.' | |||
% today.strftime('%a %b %d, %Y')) | |||
parser.add_argument('--format', '-f', type=str, default=output_formats[0], choices=output_formats, | |||
help='The format under which the information have to be output') | |||
parser.add_argument('--latitude', '-lat', type=float, default=0., | |||
help="The observer's latitude on Earth") | |||
parser.add_argument('--longitude', '-lon', type=float, default=0., | |||
@@ -22,6 +22,18 @@ from typing import Union | |||
from skyfield.api import Topos | |||
from skyfield.timelib import Time | |||
MOON_PHASES = { | |||
'NEW_MOON': 'New Moon', | |||
'FIRST_QUARTER': 'First Quarter', | |||
'FULL_MOON': 'Full Moon', | |||
'LAST_QUARTER': 'Last Quarter' | |||
} | |||
def skyfield_to_moon_phase(val: int) -> str: | |||
phases = list(MOON_PHASES.keys()) | |||
return phases[val] | |||
class Position: | |||
def __init__(self, latitude: float, longitude: float, altitude: float = 0): | |||
@@ -43,7 +55,7 @@ class AsterEphemerides: | |||
culmination_time: Union[Time, None], | |||
set_time: Union[Time, None]): | |||
self.rise_time = rise_time | |||
self.maximum_time = culmination_time | |||
self.culmination_time = culmination_time | |||
self.set_time = set_time | |||
@@ -18,13 +18,15 @@ | |||
from abc import ABC, abstractmethod | |||
import datetime | |||
import json | |||
from tabulate import tabulate | |||
from skyfield import almanac | |||
from .data import Object | |||
from skyfield.timelib import Time | |||
from numpy import int64 | |||
from .data import Object, AsterEphemerides, MOON_PHASES | |||
class Dumper(ABC): | |||
def __init__(self, ephemeris, date: datetime.date = datetime.date.today()): | |||
def __init__(self, ephemeris: dict, date: datetime.date = datetime.date.today()): | |||
self.ephemeris = ephemeris | |||
self.date = date | |||
@@ -33,10 +35,34 @@ class Dumper(ABC): | |||
pass | |||
class JsonDumper(Dumper): | |||
def to_string(self): | |||
return json.dumps(self.ephemeris, | |||
default=self._json_default, | |||
indent=4) | |||
@staticmethod | |||
def _json_default(obj): | |||
# Fixes the "TypeError: Object of type int64 is not JSON serializable" | |||
# See https://stackoverflow.com/a/50577730 | |||
if isinstance(obj, int64): | |||
return int(obj) | |||
if isinstance(obj, Time): | |||
return obj.utc_iso() | |||
if isinstance(obj, Object): | |||
obj = obj.__dict__ | |||
obj.pop('skyfield_name') | |||
return obj | |||
if isinstance(obj, AsterEphemerides): | |||
return obj.__dict__ | |||
raise TypeError('Object of type "%s" could not be integrated in the JSON' % str(type(obj))) | |||
class TextDumper(Dumper): | |||
def to_string(self): | |||
return '\n\n'.join(['Ephemerides of %s' % self.date.strftime('%A %B %d, %Y'), | |||
self.get_asters(self.ephemeris['planets']), | |||
self.get_asters(self.ephemeris['details']), | |||
self.get_moon(self.ephemeris['moon_phase']), | |||
'Note: All the hours are given in UTC.']) | |||
@@ -52,21 +78,21 @@ class TextDumper(Dumper): | |||
else: | |||
planet_rise = '-' | |||
if aster.ephemerides.maximum_time is not None: | |||
planet_maximum = aster.ephemerides.maximum_time.utc_strftime('%H:%M') | |||
if aster.ephemerides.culmination_time is not None: | |||
planet_culmination = aster.ephemerides.culmination_time.utc_strftime('%H:%M') | |||
else: | |||
planet_maximum = '-' | |||
planet_culmination = '-' | |||
if aster.ephemerides.set_time is not None: | |||
planet_set = aster.ephemerides.set_time.utc_strftime('%H:%M') | |||
else: | |||
planet_set = '-' | |||
data.append([name, planet_rise, planet_maximum, planet_set]) | |||
data.append([name, planet_rise, planet_culmination, planet_set]) | |||
return tabulate(data, headers=['Planet', 'Rise time', 'Culmination time', 'Set time'], tablefmt='simple', | |||
stralign='center', colalign=('left',)) | |||
@staticmethod | |||
def get_moon(moon): | |||
return 'Moon phase: %s' % almanac.MOON_PHASES[moon['phase']] | |||
def get_moon(moon_phase: str) -> str: | |||
return 'Moon phase: %s' % MOON_PHASES[moon_phase] |
@@ -20,7 +20,7 @@ import datetime | |||
from skyfield import almanac | |||
from skyfield.timelib import Time | |||
from .data import Object, Position, AsterEphemerides | |||
from .data import Object, Position, AsterEphemerides, skyfield_to_moon_phase | |||
from .core import get_skf_objects, get_timescale, ASTERS, MONTHS | |||
RISEN_ANGLE = -0.8333 | |||
@@ -42,13 +42,13 @@ class EphemeridesComputer: | |||
return {'rise': sunrise, 'set': sunset} | |||
@staticmethod | |||
def get_moon(year, month, day) -> dict: | |||
def get_moon(year, month, day) -> str: | |||
time1 = get_timescale().utc(year, month, day - 10) | |||
time2 = get_timescale().utc(year, month, day) | |||
_, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(get_skf_objects())) | |||
return {'phase': moon_phase[-1]} | |||
return skyfield_to_moon_phase(moon_phase[-1]) | |||
@staticmethod | |||
def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: | |||
@@ -90,7 +90,7 @@ class EphemeridesComputer: | |||
def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict: | |||
return {'moon_phase': self.get_moon(year, month, day), | |||
'planets': [self.get_asters_ephemerides_for_aster(aster, datetime.date(year, month, day), self.position) | |||
'details': [self.get_asters_ephemerides_for_aster(aster, datetime.date(year, month, day), self.position) | |||
for aster in ASTERS]} | |||
def compute_ephemerides_for_month(self, year: int, month: int) -> [dict]: | |||
@@ -0,0 +1 @@ | |||
from .dumper import * |
@@ -0,0 +1,32 @@ | |||
import unittest | |||
from kosmorrolib.data import AsterEphemerides, Planet | |||
from kosmorrolib.dumper import JsonDumper | |||
class DumperTestCase(unittest.TestCase): | |||
def test_json_dumper_returns_correct_json(self): | |||
data = self._get_data() | |||
self.assertEqual('{\n' | |||
' "moon_phase": "FULL_MOON",\n' | |||
' "details": [\n' | |||
' {\n' | |||
' "name": "Mars",\n' | |||
' "ephemerides": {\n' | |||
' "rise_time": null,\n' | |||
' "culmination_time": null,\n' | |||
' "set_time": null\n' | |||
' }\n' | |||
' }\n' | |||
' ]\n' | |||
'}', JsonDumper(data).to_string()) | |||
@staticmethod | |||
def _get_data(): | |||
return { | |||
'moon_phase': 'FULL_MOON', | |||
'details': [Planet('Mars', 'MARS', AsterEphemerides(None, None, None))] | |||
} | |||
if __name__ == '__main__': | |||
unittest.main() |