| @@ -4,7 +4,6 @@ on: [push] | |||||
| jobs: | jobs: | ||||
| build: | build: | ||||
| runs-on: ubuntu-latest | runs-on: ubuntu-latest | ||||
| steps: | steps: | ||||
| @@ -17,6 +16,9 @@ jobs: | |||||
| run: | | run: | | ||||
| pip install --upgrade pip pipenv | pip install --upgrade pip pipenv | ||||
| pipenv sync -d | pipenv sync -d | ||||
| - name: Unit tests | |||||
| run: | | |||||
| pipenv run python -m unittest -v test | |||||
| - name: Lint | - name: Lint | ||||
| run: | | run: | | ||||
| pipenv run pylint kosmorro *.py kosmorrolib/*.py | 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] | ## [Unreleased] | ||||
| ### Added | |||||
| - Update Numpy to v1.17.4 | - Update Numpy to v1.17.4 | ||||
| - Add JSON output (#6) | |||||
| ### Changed | |||||
| ### Removed | |||||
| ## [0.1.0] - 2019-11-10 | ## [0.1.0] - 2019-11-10 | ||||
| @@ -18,21 +18,13 @@ | |||||
| import argparse | import argparse | ||||
| from datetime import date | from datetime import date | ||||
| import numpy | |||||
| from kosmorrolib import dumper | from kosmorrolib import dumper | ||||
| from kosmorrolib.ephemerides import EphemeridesComputer, Position | 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(): | def main(): | ||||
| args = get_args() | |||||
| output_formats = get_dumpers() | |||||
| args = get_args(list(output_formats.keys())) | |||||
| year = args.year | year = args.year | ||||
| month = args.month | month = args.month | ||||
| day = args.day | day = args.day | ||||
| @@ -43,11 +35,18 @@ def main(): | |||||
| ephemeris = EphemeridesComputer(Position(args.latitude, args.longitude, altitude=args.altitude)) | ephemeris = EphemeridesComputer(Position(args.latitude, args.longitude, altitude=args.altitude)) | ||||
| ephemerides = ephemeris.compute_ephemerides(year, month, day) | ephemerides = ephemeris.compute_ephemerides(year, month, day) | ||||
| dump = dumper.TextDumper(ephemerides) | |||||
| dump = output_formats[args.format](ephemerides) | |||||
| print(dump.to_string()) | 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() | today = date.today() | ||||
| parser = argparse.ArgumentParser(description='Compute the ephemerides for a given date, at a given position' | 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.' | ' observer positioned at coordinates (0,0), with an altitude of 0.' | ||||
| % today.strftime('%a %b %d, %Y')) | % 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., | parser.add_argument('--latitude', '-lat', type=float, default=0., | ||||
| help="The observer's latitude on Earth") | help="The observer's latitude on Earth") | ||||
| parser.add_argument('--longitude', '-lon', type=float, default=0., | parser.add_argument('--longitude', '-lon', type=float, default=0., | ||||
| @@ -22,6 +22,18 @@ from typing import Union | |||||
| from skyfield.api import Topos | from skyfield.api import Topos | ||||
| from skyfield.timelib import Time | 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: | class Position: | ||||
| def __init__(self, latitude: float, longitude: float, altitude: float = 0): | def __init__(self, latitude: float, longitude: float, altitude: float = 0): | ||||
| @@ -43,7 +55,7 @@ class AsterEphemerides: | |||||
| culmination_time: Union[Time, None], | culmination_time: Union[Time, None], | ||||
| set_time: Union[Time, None]): | set_time: Union[Time, None]): | ||||
| self.rise_time = rise_time | self.rise_time = rise_time | ||||
| self.maximum_time = culmination_time | |||||
| self.culmination_time = culmination_time | |||||
| self.set_time = set_time | self.set_time = set_time | ||||
| @@ -18,13 +18,15 @@ | |||||
| from abc import ABC, abstractmethod | from abc import ABC, abstractmethod | ||||
| import datetime | import datetime | ||||
| import json | |||||
| from tabulate import tabulate | 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): | 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.ephemeris = ephemeris | ||||
| self.date = date | self.date = date | ||||
| @@ -33,10 +35,34 @@ class Dumper(ABC): | |||||
| pass | 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): | class TextDumper(Dumper): | ||||
| def to_string(self): | def to_string(self): | ||||
| return '\n\n'.join(['Ephemerides of %s' % self.date.strftime('%A %B %d, %Y'), | 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']), | self.get_moon(self.ephemeris['moon_phase']), | ||||
| 'Note: All the hours are given in UTC.']) | 'Note: All the hours are given in UTC.']) | ||||
| @@ -52,21 +78,21 @@ class TextDumper(Dumper): | |||||
| else: | else: | ||||
| planet_rise = '-' | 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: | else: | ||||
| planet_maximum = '-' | |||||
| planet_culmination = '-' | |||||
| if aster.ephemerides.set_time is not None: | if aster.ephemerides.set_time is not None: | ||||
| planet_set = aster.ephemerides.set_time.utc_strftime('%H:%M') | planet_set = aster.ephemerides.set_time.utc_strftime('%H:%M') | ||||
| else: | else: | ||||
| planet_set = '-' | 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', | return tabulate(data, headers=['Planet', 'Rise time', 'Culmination time', 'Set time'], tablefmt='simple', | ||||
| stralign='center', colalign=('left',)) | stralign='center', colalign=('left',)) | ||||
| @staticmethod | @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 import almanac | ||||
| from skyfield.timelib import Time | 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 | from .core import get_skf_objects, get_timescale, ASTERS, MONTHS | ||||
| RISEN_ANGLE = -0.8333 | RISEN_ANGLE = -0.8333 | ||||
| @@ -42,13 +42,13 @@ class EphemeridesComputer: | |||||
| return {'rise': sunrise, 'set': sunset} | return {'rise': sunrise, 'set': sunset} | ||||
| @staticmethod | @staticmethod | ||||
| def get_moon(year, month, day) -> dict: | |||||
| def get_moon(year, month, day) -> str: | |||||
| time1 = get_timescale().utc(year, month, day - 10) | time1 = get_timescale().utc(year, month, day - 10) | ||||
| time2 = get_timescale().utc(year, month, day) | time2 = get_timescale().utc(year, month, day) | ||||
| _, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(get_skf_objects())) | _, 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 | @staticmethod | ||||
| def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: | 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: | def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict: | ||||
| return {'moon_phase': self.get_moon(year, month, day), | 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]} | for aster in ASTERS]} | ||||
| def compute_ephemerides_for_month(self, year: int, month: int) -> [dict]: | 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() | |||||