Skip to content

Commit

Permalink
feat: add support for timezones
Browse files Browse the repository at this point in the history
  • Loading branch information
Jérôme Deuchnord committed Feb 17, 2020
1 parent b19055c commit d7730bd
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 106 deletions.
3 changes: 2 additions & 1 deletion .pylintrc
Expand Up @@ -146,7 +146,8 @@ disable=print-statement,
too-many-branches,
too-few-public-methods,
protected-access,
unnecessary-comprehension
unnecessary-comprehension,
too-many-arguments

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
2 changes: 2 additions & 0 deletions .scripts/tests-e2e.sh
Expand Up @@ -79,6 +79,8 @@ assertSuccess "$PIP_BIN install dist/kosmorro-$VERSION.tar.gz" "CI"
assertSuccess kosmorro
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 27 -m 1 -y 2020"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 27 -m 1 -y 2020 --timezone=1"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 27 -m 1 -y 2020 --timezone=-1"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 27 -m 1 -y 2020 --format=json"
assertFailure "kosmorro --latitude=50.5876 --longitude=3.0624 -d 27 -m 1 -y 2020 --format=pdf"
# Missing dependencies, should fail
Expand Down
4 changes: 3 additions & 1 deletion kosmorrolib/core.py
Expand Up @@ -95,7 +95,9 @@ def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonP
next_phase_time = times[j]
break

return MoonPhase(current_phase, current_phase_time, next_phase_time)
return MoonPhase(current_phase,
current_phase_time.utc_datetime() if current_phase_time is not None else None,
next_phase_time.utc_datetime() if next_phase_time is not None else None)


def flatten_list(the_list: list):
Expand Down
13 changes: 7 additions & 6 deletions kosmorrolib/data.py
Expand Up @@ -18,9 +18,9 @@

from abc import ABC, abstractmethod
from typing import Union
from datetime import datetime

from skyfield.api import Topos
from skyfield.timelib import Time

from .i18n import _

Expand All @@ -42,7 +42,7 @@


class MoonPhase:
def __init__(self, identifier: str, time: Union[Time, None], next_phase_date: Union[Time, None]):
def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[datetime, None]):
if identifier not in MOON_PHASES.keys():
raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()),
identifier))
Expand Down Expand Up @@ -87,9 +87,9 @@ def get_planet_topos(self) -> Topos:

class AsterEphemerides:
def __init__(self,
rise_time: Union[Time, None],
culmination_time: Union[Time, None],
set_time: Union[Time, None]):
rise_time: Union[datetime, None],
culmination_time: Union[datetime, None],
set_time: Union[datetime, None]):
self.rise_time = rise_time
self.culmination_time = culmination_time
self.set_time = set_time
Expand Down Expand Up @@ -141,7 +141,8 @@ def get_type(self) -> str:


class Event:
def __init__(self, event_type: str, objects: [Object], start_time: Time, end_time: Union[Time, None] = None):
def __init__(self, event_type: str, objects: [Object], start_time: datetime,
end_time: Union[datetime, None] = None):
if event_type not in EVENTS.keys():
raise ValueError('event_type parameter must be one of the following: %s (got %s)' % (
', '.join(EVENTS.keys()),
Expand Down
94 changes: 76 additions & 18 deletions kosmorrolib/dumper.py
Expand Up @@ -21,7 +21,6 @@
import json
import os
from tabulate import tabulate
from skyfield.timelib import Time
from numpy import int64
from termcolor import colored
from .data import Object, AsterEphemerides, MoonPhase, Event
Expand All @@ -35,17 +34,52 @@

FULL_DATE_FORMAT = _('{day_of_week} {month} {day_number}, {year}').format(day_of_week='%A', month='%B',
day_number='%d', year='%Y')
SHORT_DATETIME_FORMAT = _('{month} {day_number}, {hours}:{minutes}').format(month='%b', day_number='%d',
hours='%H', minutes='%M')
TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M')


class Dumper(ABC):
def __init__(self, ephemeris: dict, events: [Event], date: datetime.date = datetime.date.today(),
def __init__(self, ephemeris: dict, events: [Event], date: datetime.date = datetime.date.today(), timezone: int = 0,
with_colors: bool = True):
self.ephemeris = ephemeris
self.events = events
self.date = date
self.timezone = timezone
self.with_colors = with_colors

if self.timezone != 0:
self._convert_dates_to_timezones()

def _convert_dates_to_timezones(self):
if self.ephemeris['moon_phase'].time is not None:
self.ephemeris['moon_phase'].time = self._datetime_to_timezone(self.ephemeris['moon_phase'].time)
if self.ephemeris['moon_phase'].next_phase_date is not None:
self.ephemeris['moon_phase'].next_phase_date = self._datetime_to_timezone(
self.ephemeris['moon_phase'].next_phase_date)

for aster in self.ephemeris['details']:
if aster.ephemerides.rise_time is not None:
aster.ephemerides.rise_time = self._datetime_to_timezone(aster.ephemerides.rise_time)
if aster.ephemerides.culmination_time is not None:
aster.ephemerides.culmination_time = self._datetime_to_timezone(aster.ephemerides.culmination_time)
if aster.ephemerides.set_time is not None:
aster.ephemerides.set_time = self._datetime_to_timezone(aster.ephemerides.set_time)

for event in self.events:
event.start_time = self._datetime_to_timezone(event.start_time)
if event.end_time is not None:
event.end_time = self._datetime_to_timezone(event.end_time)

def _datetime_to_timezone(self, time: datetime.datetime):
return time.replace(tzinfo=datetime.timezone.utc).astimezone(
tz=datetime.timezone(
datetime.timedelta(
hours=self.timezone
)
)
)

def get_date_as_string(self, capitalized: bool = False) -> str:
date = self.date.strftime(FULL_DATE_FORMAT)

Expand Down Expand Up @@ -77,8 +111,8 @@ def _json_default(obj):
# See https://stackoverflow.com/a/50577730
if isinstance(obj, int64):
return int(obj)
if isinstance(obj, Time):
return obj.utc_iso()
if isinstance(obj, datetime.datetime):
return obj.isoformat()
if isinstance(obj, Object):
obj = obj.__dict__
obj.pop('skyfield_name')
Expand Down Expand Up @@ -113,7 +147,14 @@ def to_string(self):
text.append('\n'.join([self.style(_('Expected events:'), 'h2'),
self.get_events(self.events)]))

text.append(self.style(_('Note: All the hours are given in UTC.'), 'em'))
if self.timezone == 0:
text.append(self.style(_('Note: All the hours are given in UTC.'), 'em'))
else:
tz_offset = str(self.timezone)
if self.timezone > 0:
tz_offset = ''.join(['+', tz_offset])
text.append(self.style(_('Note: All the hours are given in the UTC{offset} timezone.').format(
offset=tz_offset), 'em'))

return '\n\n'.join(text)

Expand All @@ -138,17 +179,21 @@ def get_asters(self, asters: [Object]) -> str:
name = self.style(aster.name, 'th')

if aster.ephemerides.rise_time is not None:
planet_rise = aster.ephemerides.rise_time.utc_strftime(TIME_FORMAT)
time_fmt = TIME_FORMAT if aster.ephemerides.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT
planet_rise = aster.ephemerides.rise_time.strftime(time_fmt)
else:
planet_rise = '-'

if aster.ephemerides.culmination_time is not None:
planet_culmination = aster.ephemerides.culmination_time.utc_strftime(TIME_FORMAT)
time_fmt = TIME_FORMAT if aster.ephemerides.culmination_time.day == self.date.day \
else SHORT_DATETIME_FORMAT
planet_culmination = aster.ephemerides.culmination_time.strftime(time_fmt)
else:
planet_culmination = '-'

if aster.ephemerides.set_time is not None:
planet_set = aster.ephemerides.set_time.utc_strftime(TIME_FORMAT)
time_fmt = TIME_FORMAT if aster.ephemerides.set_time.day == self.date.day else SHORT_DATETIME_FORMAT
planet_set = aster.ephemerides.set_time.strftime(time_fmt)
else:
planet_set = '-'

Expand All @@ -164,7 +209,8 @@ def get_events(self, events: [Event]) -> str:
data = []

for event in events:
data.append([self.style(event.start_time.utc_strftime(TIME_FORMAT), 'th'),
time_fmt = TIME_FORMAT if event.start_time.day == self.date.day else SHORT_DATETIME_FORMAT
data.append([self.style(event.start_time.strftime(time_fmt), 'th'),
event.get_description()])

return tabulate(data, tablefmt='plain', stralign='left')
Expand All @@ -173,8 +219,8 @@ def get_moon(self, moon_phase: MoonPhase) -> str:
current_moon_phase = ' '.join([self.style(_('Moon phase:'), 'strong'), moon_phase.get_phase()])
new_moon_phase = _('{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}').format(
next_moon_phase=moon_phase.get_next_phase(),
next_moon_phase_date=moon_phase.next_phase_date.utc_strftime(FULL_DATE_FORMAT),
next_moon_phase_time=moon_phase.next_phase_date.utc_strftime(TIME_FORMAT)
next_moon_phase_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT),
next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_FORMAT)
)

return '\n'.join([current_moon_phase, new_moon_phase])
Expand Down Expand Up @@ -214,8 +260,11 @@ def _make_document(self, template: str) -> str:
.replace('+++INTRODUCTION+++',
'\n\n'.join([
_("This document summarizes the ephemerides and the events of {date}. "
"It aims to help you to prepare your observation session.").format(
date=self.get_date_as_string()),
"It aims to help you to prepare your observation session. "
"All the hours are given in {timezone}.").format(
date=self.get_date_as_string(),
timezone='UTC+%d timezone' % self.timezone if self.timezone != 0 else 'UTC'
),
_("Don't forget to check the weather forecast before you go out with your material.")
])) \
.replace('+++SECTION-EPHEMERIDES+++', _('Ephemerides of the day')) \
Expand All @@ -237,17 +286,21 @@ def _make_ephemerides(self) -> str:

for aster in self.ephemeris['details']:
if aster.ephemerides.rise_time is not None:
aster_rise = aster.ephemerides.rise_time.utc_strftime(TIME_FORMAT)
time_fmt = TIME_FORMAT if aster.ephemerides.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT
aster_rise = aster.ephemerides.rise_time.strftime(time_fmt)
else:
aster_rise = '-'

if aster.ephemerides.culmination_time is not None:
aster_culmination = aster.ephemerides.culmination_time.utc_strftime(TIME_FORMAT)
time_fmt = TIME_FORMAT if aster.ephemerides.culmination_time.day == self.date.day\
else SHORT_DATETIME_FORMAT
aster_culmination = aster.ephemerides.culmination_time.strftime(time_fmt)
else:
aster_culmination = '-'

if aster.ephemerides.set_time is not None:
aster_set = aster.ephemerides.set_time.utc_strftime(TIME_FORMAT)
time_fmt = TIME_FORMAT if aster.ephemerides.set_time.day == self.date.day else SHORT_DATETIME_FORMAT
aster_set = aster.ephemerides.set_time.strftime(time_fmt)
else:
aster_set = '-'

Expand All @@ -262,7 +315,7 @@ def _make_events(self) -> str:
latex = []

for event in self.events:
latex.append(r'\event{%s}{%s}' % (event.start_time.utc_strftime(TIME_FORMAT),
latex.append(r'\event{%s}{%s}' % (event.start_time.strftime(TIME_FORMAT),
event.get_description()))

return ''.join(latex)
Expand All @@ -288,9 +341,14 @@ def _remove_section(document: str, section: str):


class PdfDumper(Dumper):
def __init__(self, ephemerides, events, date=datetime.datetime.now(), timezone=0, with_colors=True):
super(PdfDumper, self).__init__(ephemerides, events, date=date, timezone=0, with_colors=with_colors)
self.timezone = timezone

def to_string(self):
try:
latex_dumper = _LatexDumper(self.ephemeris, self.events, self.date, self.with_colors)
latex_dumper = _LatexDumper(self.ephemeris, self.events,
date=self.date, timezone=self.timezone, with_colors=self.with_colors)
return self._compile(latex_dumper.to_string())
except RuntimeError:
raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not"
Expand Down
5 changes: 5 additions & 0 deletions kosmorrolib/ephemerides.py
Expand Up @@ -100,6 +100,11 @@ def is_risen(time: Time) -> bool:

culmination_time = culmination_time[0] if culmination_time is not None else None

# Convert the Time instances to Python datetime objects
rise_time = rise_time.utc_datetime().replace(microsecond=0)
culmination_time = culmination_time.utc_datetime().replace(microsecond=0)
set_time = set_time.utc_datetime().replace(microsecond=0)

aster.ephemerides = AsterEphemerides(rise_time, culmination_time, set_time)
return aster

Expand Down
6 changes: 3 additions & 3 deletions kosmorrolib/events.py
Expand Up @@ -57,7 +57,7 @@ def is_in_conjunction(time: Time):
times, _ = find_discrete(start_time, end_time, is_in_conjunction)

for time in times:
conjunctions.append(Event('CONJUNCTION', [aster1, aster2], time))
conjunctions.append(Event('CONJUNCTION', [aster1, aster2], time.utc_datetime()))

computed.append(aster1)

Expand Down Expand Up @@ -86,7 +86,7 @@ def is_oppositing(time: Time) -> [bool]:

times, _ = find_discrete(start_time, end_time, is_oppositing)
for time in times:
events.append(Event('OPPOSITION', [aster], time))
events.append(Event('OPPOSITION', [aster], time.utc_datetime()))

return events

Expand All @@ -98,4 +98,4 @@ def search_events(date: date_type) -> [Event]:
return sorted(flatten_list([
_search_oppositions(start_time, end_time),
_search_conjunction(start_time, end_time)
]), key=lambda event: event.start_time.utc_datetime())
]), key=lambda event: event.start_time)

0 comments on commit d7730bd

Please sign in to comment.