From da64382b3bab18776e16e24b597ce4a03edfe723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Deuchnord?= Date: Tue, 12 Nov 2019 20:34:54 +0100 Subject: [PATCH 1/2] Add JSON output --- kosmorro | 25 +++++++++++++------------ kosmorrolib/data.py | 12 ++++++++++++ kosmorrolib/dumper.py | 32 +++++++++++++++++++++++++++++--- kosmorrolib/ephemerides.py | 4 ++-- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/kosmorro b/kosmorro index f478abe..224b8cb 100644 --- a/kosmorro +++ b/kosmorro @@ -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., diff --git a/kosmorrolib/data.py b/kosmorrolib/data.py index 89695a4..2ba310d 100644 --- a/kosmorrolib/data.py +++ b/kosmorrolib/data.py @@ -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): diff --git a/kosmorrolib/dumper.py b/kosmorrolib/dumper.py index b5f039c..d2b8c9a 100644 --- a/kosmorrolib/dumper.py +++ b/kosmorrolib/dumper.py @@ -18,9 +18,11 @@ 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): @@ -33,6 +35,30 @@ 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'), @@ -69,4 +95,4 @@ class TextDumper(Dumper): @staticmethod def get_moon(moon): - return 'Moon phase: %s' % almanac.MOON_PHASES[moon['phase']] + return 'Moon phase: %s' % MOON_PHASES[moon['phase']] diff --git a/kosmorrolib/ephemerides.py b/kosmorrolib/ephemerides.py index 93b5e35..0c2b42a 100644 --- a/kosmorrolib/ephemerides.py +++ b/kosmorrolib/ephemerides.py @@ -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 @@ -48,7 +48,7 @@ class EphemeridesComputer: _, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(get_skf_objects())) - return {'phase': moon_phase[-1]} + return {'phase': skyfield_to_moon_phase(moon_phase[-1])} @staticmethod def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: From a8dfe3bd1112f5ce0b1ae04bbf9c02dc7d2acfb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Deuchnord?= Date: Tue, 12 Nov 2019 21:35:16 +0100 Subject: [PATCH 2/2] Add unit tests, fix variable names --- .github/workflows/pythonapp.yml | 4 +++- CHANGELOG.md | 11 +++++++++++ kosmorrolib/data.py | 2 +- kosmorrolib/dumper.py | 16 ++++++++-------- kosmorrolib/ephemerides.py | 6 +++--- test/__init__.py | 1 + test/dumper.py | 32 ++++++++++++++++++++++++++++++++ 7 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 test/__init__.py create mode 100644 test/dumper.py diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 9f18186..5023db7 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2a95c..1161159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/kosmorrolib/data.py b/kosmorrolib/data.py index 2ba310d..159ff90 100644 --- a/kosmorrolib/data.py +++ b/kosmorrolib/data.py @@ -55,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 diff --git a/kosmorrolib/dumper.py b/kosmorrolib/dumper.py index d2b8c9a..1e49c47 100644 --- a/kosmorrolib/dumper.py +++ b/kosmorrolib/dumper.py @@ -26,7 +26,7 @@ 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 @@ -62,7 +62,7 @@ class JsonDumper(Dumper): 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.']) @@ -78,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' % MOON_PHASES[moon['phase']] + def get_moon(moon_phase: str) -> str: + return 'Moon phase: %s' % MOON_PHASES[moon_phase] diff --git a/kosmorrolib/ephemerides.py b/kosmorrolib/ephemerides.py index 0c2b42a..eb0b1d5 100644 --- a/kosmorrolib/ephemerides.py +++ b/kosmorrolib/ephemerides.py @@ -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': skyfield_to_moon_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]: diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..3b6d8eb --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +from .dumper import * \ No newline at end of file diff --git a/test/dumper.py b/test/dumper.py new file mode 100644 index 0000000..a922e0e --- /dev/null +++ b/test/dumper.py @@ -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()