Browse Source

fix: handle out of range date error

tags/v0.8.1
Jérôme Deuchnord 4 years ago
parent
commit
c39cd3aefa
No known key found for this signature in database GPG Key ID: 72F9D1A7272D53DD
20 changed files with 280 additions and 131 deletions
  1. +1
    -1
      .github/workflows/unit-tests.yml
  2. +2
    -0
      .scripts/tests-e2e.sh
  3. +1
    -1
      CONTRIBUTING.md
  4. +8
    -0
      Makefile
  5. +1
    -1
      Pipfile
  6. +1
    -1
      Pipfile.lock
  7. BIN
      kosmorrolib/assets/moonphases/png/unknown.png
  8. +31
    -0
      kosmorrolib/assets/moonphases/svg/unknown.svg
  9. +3
    -2
      kosmorrolib/data.py
  10. +11
    -7
      kosmorrolib/dumper.py
  11. +51
    -31
      kosmorrolib/ephemerides.py
  12. +16
    -5
      kosmorrolib/events.py
  13. +13
    -0
      kosmorrolib/exceptions.py
  14. +7
    -0
      kosmorrolib/i18n.py
  15. +79
    -59
      kosmorrolib/locales/messages.pot
  16. +36
    -22
      kosmorrolib/main.py
  17. +1
    -1
      setup.py
  18. +4
    -0
      test/dumper.py
  19. +9
    -0
      test/ephemerides.py
  20. +5
    -0
      test/events.py

+ 1
- 1
.github/workflows/unit-tests.yml View File

@@ -20,5 +20,5 @@ jobs:
env: env:
COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
run: | run: |
pipenv run python -m coverage run -m unittest test
make test
COVERALLS_REPO_TOKEN=$COVERALLS_TOKEN pipenv run coveralls COVERALLS_REPO_TOKEN=$COVERALLS_TOKEN pipenv run coveralls

+ 2
- 0
.scripts/tests-e2e.sh View File

@@ -81,6 +81,8 @@ assertSuccess "kosmorro -h"
assertSuccess "kosmorro -d 2020-01-27" assertSuccess "kosmorro -d 2020-01-27"
assertFailure "kosmorro -d yolo-yo-lo" assertFailure "kosmorro -d yolo-yo-lo"
assertFailure "kosmorro -d 2020-13-32" assertFailure "kosmorro -d 2020-13-32"
assertFailure "kosmorro --date=1789-05-05"
assertFailure "kosmorro --date=3000-01-01"
assertSuccess "kosmorro --date='+3y 5m3d'" assertSuccess "kosmorro --date='+3y 5m3d'"
assertSuccess "kosmorro --date='-1y3d'" assertSuccess "kosmorro --date='-1y3d'"
assertFailure "kosmorro --date='+3d4m" assertFailure "kosmorro --date='+3d4m"


+ 1
- 1
CONTRIBUTING.md View File

@@ -80,7 +80,7 @@ Kosmorro's unit tests use Python's official `unittest` module. They live in the
You can also run them by invoking the following command: You can also run them by invoking the following command:


```shell ```shell
pipenv run python -m unittest test
make test
``` ```


Note: there are currently some memory leaks in the unit tests, making the result quite difficult to read. I am working to fix this. Note: there are currently some memory leaks in the unit tests, making the result quite difficult to read. I am working to fix this.


+ 8
- 0
Makefile View File

@@ -1,3 +1,11 @@
.PHONY: test

test:
unset KOSMORRO_LATITUDE; \
unset KOSMORRO_LONGITUDE; \
unset KOSMORRO_TIMEZONE; \
LANG=C pipenv run python3 -m coverage run -m unittest test

build: build:
ronn --roff manpage/kosmorro.1.md ronn --roff manpage/kosmorro.1.md
ronn --roff manpage/kosmorro.7.md ronn --roff manpage/kosmorro.7.md


+ 1
- 1
Pipfile View File

@@ -11,7 +11,7 @@ unittest-data-provider = "*"
coveralls = "*" coveralls = "*"


[packages] [packages]
skyfield = ">=1.13.0,<2.0.0"
skyfield = ">=1.21.0,<2.0.0"
tabulate = "*" tabulate = "*"
numpy = ">=1.17.0,<2.0.0" numpy = ">=1.17.0,<2.0.0"
termcolor = "*" termcolor = "*"


+ 1
- 1
Pipfile.lock View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "afd17771258a86b04cab79d21d2afbfb1faf44bbe6e760da9140e72b0999a5b1"
"sha256": "9ed5ee6bbfde75ee77c89fdc09a793f5c00f9782968dc310e1eb8d3386378d9e"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {


BIN
kosmorrolib/assets/moonphases/png/unknown.png View File

Before After
Width: 312  |  Height: 312  |  Size: 26 KiB

+ 31
- 0
kosmorrolib/assets/moonphases/svg/unknown.svg View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="92.682mm" height="92.684mm" version="1.1" viewBox="0 0 92.682 92.684" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<radialGradient id="radialGradient4622" cx="245.39" cy="354.57" r="234.94" fx="248.94" gradientTransform="translate(-41.85,-50.912)" gradientUnits="userSpaceOnUse">
<stop stop-color="#b7c3cc" offset="0"/>
<stop stop-color="#001e29" offset="1"/>
</radialGradient>
</defs>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-15.647 -114.59)">
<g transform="matrix(.26458 0 0 .26458 7.4191 107.94)">
<g fill-rule="evenodd">
<path d="m362.49 200.25a156.25 156.25 0 1 1-312.5 0 156.25 156.25 0 1 1 312.5 0z" fill="#000024" stroke-width="1.0102"/>
<path d="m164.32 56.438c-25.06 6.447-47.957 20.982-65.936 39.456-33.589 34.689-49.415 85.84-39.274 133.3 7.7684 38.332 30.319 73.818 63.261 95.34 39.082 26.542 90.824 33.344 135.13 16.523 44.589-15.935 80.583-53.959 92.968-99.846 14.03-47.365 2.42-101-30.04-138.25-27.522-32.74-69.296-52.414-112.07-52.8-2.4338-0.40982-30.229 1.5565-44.039 6.2796z" fill="url(#radialGradient4622)"/>
<path d="m297.02 188.91c-10.37 17.497 15.281 19.939 24.511 25.141-3.4576-31.524 10.512-20.807 13.141-30.822-2.4603-15.476-20.24-18.289-30.195-28.185-0.36892-19.454-33.444-17.876-30.691-31.892-5.3458-59.729-15.582-5.0632-17.324-14.89-11.086 12.896-14.512-1.6954-11.166-10.995 0.26364-11.365-18.639-11.91-25.607-6.5757-5.8842 3.1388-14.403 3.9638-17.373 11.046-13.66 2.7421-12.28-17.783-26.048-16.916-15.276-6.4266-21.571-23.77-33.99-13.45-8.9365 2.8834-27.358 38.752-33.694 23.765-13.5-9.5163-20.451 15.279-20.026 25.654-6.3485 13.802-17.899 25.142-24.397 39.356-8.0969 13.6-8.0574 31.852-2.7715 46.369 16.911-8.3049 18.513 27.307 24.313 42.266 8.439 10.014 21.898-12.821 25.417-6.9099 22.751 14.162-5.2534 18.337-1.2166 29.083 4.539 15.527 24.377 5.6814 35.735 9.2032 4.7599 1.8134 10.807 12.467 13.777 3.1751-1.815-6.8086-5.6283-13.445 4.7747-13.371 7.3152-6.3929 17.349-5.3491 24.821-7.2415 9.0393-11.572-10.092-19.333-4.8746-30.443 2.8779-11.759-12.73-8.4941-10.462-18.846-7.0124-5.8828-11.048-16.424 0.22233-20.807 7.9487-5.15 29.707 5.1288 24.31-11.199-6.8963-0.46855-16.33-6.6026-8.8168-14.327 10.756-7.5046 26.836 6.3362 36.887-4.9453 1.0925-5.4659 4.6002-20.74 11.751-10.347-2.0036 12.706 4.4139 23.496 16.934 28.605 5.6256 3.7827 11.442 24.659 18.963 12.144-10.579-15.807-1.3253-36.935 21.493-9.7167-0.27777 2.0581-0.63554 4.9085 1.6013 6.0725zm-112.57-59.732c-3.3727-9.642-15.855 0.88715-7.0124 5.9448-1.4949 18.168-23.383 19.29-37.295 19.685-8.7173 4.2923-20.669 6.5967-27.583 10.788-1.5637 11.397 17.747 10.622 15.712 22.827 8.4646 11.957 22.01 19.653 28.373 33.172 12.227 10.86 6.5682-16.046-1.1103-17.921-11.846-4.4912-26.133-24.017-7.6284-30.074 12.233-3.4412 6.304-25.137 22.916-20.183 15.144 3.9218 14.776-14.074 24.42-20.796 2.2128-9.6425-5.7023-6.6942-10.791-3.4427z" fill-opacity=".25126"/>
</g>
<path d="m358.45 201.09c0 83.168-68.058 150.67-151.91 150.67s-151.91-67.499-151.91-150.67 64.682-150.67 151.91-150.67c89.933 0 151.91 67.499 151.91 150.67z" fill="#000024" fill-opacity=".86432"/>
</g>
</g>
<g fill="#fff" fill-opacity=".33333" aria-label="?">
<path d="m37.427 57.422q0-4.3215 1.4111-7.7611 1.4111-3.4396 5.8208-5.9972 1.3229-0.79375 3.2632-1.7639 1.9403-1.0583 3.7924-2.4694 1.8521-1.4111 3.0868-3.2632 1.3229-1.9403 1.3229-4.4097 0-1.8521-0.70556-3.2632-0.70556-1.4111-1.8521-2.3812t-2.734-1.4111q-1.4993-0.52917-3.0868-0.52917-1.9403 0-3.5278 0.70556-1.5875 0.61736-2.9104 1.6757-1.2347 0.97014-2.2931 2.2931-0.97014 1.2347-1.6757 2.3812l-7.6729-5.2917q1.1465-2.734 2.9986-4.8507 1.9403-2.1167 4.3215-3.5278 2.4694-1.4993 5.2917-2.2049 2.8222-0.79375 5.8208-0.79375 3.3514 0 6.6146 0.97014 3.2632 0.97014 5.8208 3.0868 2.6458 2.1167 4.2333 5.4681 1.6757 3.2632 1.6757 7.8493 0 2.8222-0.70556 5.0271-0.61736 2.1167-1.8521 3.8806-1.2347 1.7639-2.9104 3.175-1.6757 1.4111-3.7924 2.734-1.7639 1.0583-3.5278 2.0285-1.7639 0.97014-3.2632 2.2049-1.4111 1.1465-2.3812 2.734-0.88194 1.4993-0.88194 3.7042zm0.17639 20.549v-12.171h9.7896v12.171z" fill="#fff" fill-opacity=".33333" stroke-width=".26458"/>
</g>
</svg>

+ 3
- 2
kosmorrolib/data.py View File

@@ -36,7 +36,8 @@ MOON_PHASES = {
'FULL_MOON': _('Full Moon'), 'FULL_MOON': _('Full Moon'),
'WANING_GIBBOUS': _('Waning gibbous'), 'WANING_GIBBOUS': _('Waning gibbous'),
'LAST_QUARTER': _('Last Quarter'), 'LAST_QUARTER': _('Last Quarter'),
'WANING_CRESCENT': _('Waning crescent')
'WANING_CRESCENT': _('Waning crescent'),
'UNKNOWN': _('Unavailable')
} }


EVENTS = { EVENTS = {
@@ -54,7 +55,7 @@ class Serializable(ABC):




class MoonPhase(Serializable): class MoonPhase(Serializable):
def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[datetime, None]):
def __init__(self, identifier: str, time: datetime = None, next_phase_date: datetime = None):
if identifier not in MOON_PHASES.keys(): if identifier not in MOON_PHASES.keys():
raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()), raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()),
identifier)) identifier))


+ 11
- 7
kosmorrolib/dumper.py View File

@@ -25,7 +25,7 @@ from tabulate import tabulate
from numpy import int64 from numpy import int64
from termcolor import colored from termcolor import colored
from .data import ASTERS, Object, AsterEphemerides, MoonPhase, Event from .data import ASTERS, Object, AsterEphemerides, MoonPhase, Event
from .i18n import _
from .i18n import _, FULL_DATE_FORMAT, SHORT_DATETIME_FORMAT, TIME_FORMAT
from .version import VERSION from .version import VERSION
from .exceptions import UnavailableFeatureError from .exceptions import UnavailableFeatureError
try: try:
@@ -33,12 +33,6 @@ try:
except ImportError: except ImportError:
build_pdf = None build_pdf = None


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): class Dumper(ABC):
def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None, def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None,
@@ -60,6 +54,9 @@ class Dumper(ABC):


return date return date


def __str__(self):
return self.to_string()

@abstractmethod @abstractmethod
def to_string(self): def to_string(self):
pass pass
@@ -189,6 +186,9 @@ class TextDumper(Dumper):
return tabulate(data, tablefmt='plain', stralign='left') return tabulate(data, tablefmt='plain', stralign='left')


def get_moon(self, moon_phase: MoonPhase) -> str: def get_moon(self, moon_phase: MoonPhase) -> str:
if moon_phase is None:
return _('Moon phase is unavailable for this date.')

current_moon_phase = ' '.join([self.style(_('Moon phase:'), 'strong'), moon_phase.get_phase()]) 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( 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_name(), next_moon_phase=moon_phase.get_next_phase_name(),
@@ -212,6 +212,10 @@ class _LatexDumper(Dumper):
def _make_document(self, template: str) -> str: def _make_document(self, template: str) -> str:
kosmorro_logo_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), kosmorro_logo_path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'assets', 'png', 'kosmorro-logo.png') 'assets', 'png', 'kosmorro-logo.png')

if self.moon_phase is None:
self.moon_phase = MoonPhase('UNKNOWN')

moon_phase_graphics = os.path.join(os.path.abspath(os.path.dirname(__file__)), moon_phase_graphics = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'assets', 'moonphases', 'png', 'assets', 'moonphases', 'png',
'.'.join([self.moon_phase.identifier.lower().replace('_', '-'), '.'.join([self.moon_phase.identifier.lower().replace('_', '-'),


+ 51
- 31
kosmorrolib/ephemerides.py View File

@@ -21,15 +21,17 @@ import datetime
from skyfield.searchlib import find_discrete, find_maxima from skyfield.searchlib import find_discrete, find_maxima
from skyfield.timelib import Time from skyfield.timelib import Time
from skyfield.constants import tau from skyfield.constants import tau
from skyfield.errors import EphemerisRangeError


from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS, skyfield_to_moon_phase from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS, skyfield_to_moon_phase
from .dateutil import translate_to_timezone from .dateutil import translate_to_timezone
from .core import get_skf_objects, get_timescale, get_iau2000b from .core import get_skf_objects, get_timescale, get_iau2000b
from .exceptions import OutOfRangeDateError


RISEN_ANGLE = -0.8333 RISEN_ANGLE = -0.8333




def get_moon_phase(compute_date: datetime.date) -> MoonPhase:
def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase:
earth = get_skf_objects()['earth'] earth = get_skf_objects()['earth']
moon = get_skf_objects()['moon'] moon = get_skf_objects()['moon']
sun = get_skf_objects()['sun'] sun = get_skf_objects()['sun']
@@ -47,7 +49,16 @@ def get_moon_phase(compute_date: datetime.date) -> MoonPhase:
time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10) time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10)
time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10) time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10)


times, phase = find_discrete(time1, time2, moon_phase_at)
try:
times, phase = find_discrete(time1, time2, moon_phase_at)
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)

raise OutOfRangeDateError(start, end)


return skyfield_to_moon_phase(times, phase, today) return skyfield_to_moon_phase(times, phase, today)


@@ -71,34 +82,43 @@ def get_ephemerides(date: datetime.date, position: Position, timezone: int = 0)
start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) start_time = get_timescale().utc(date.year, date.month, date.day, -timezone)
end_time = get_timescale().utc(date.year, date.month, date.day, 23 - timezone, 59, 59) end_time = get_timescale().utc(date.year, date.month, date.day, 23 - timezone, 59, 59)


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./3600/24, num=12)
culmination_time = culmination_time[0] if len(culmination_time) > 0 else None
except ValueError:
culmination_time = None

if len(rise_times) == 2:
rise_time = rise_times[0 if arr[0] else 1]
set_time = rise_times[1 if not arr[1] else 0]
else:
rise_time = rise_times[0] if arr[0] else None
set_time = rise_times[0] if not arr[0] else None

# Convert the Time instances to Python datetime objects
if rise_time is not None:
rise_time = translate_to_timezone(rise_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

if culmination_time is not None:
culmination_time = translate_to_timezone(culmination_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

if set_time is not None:
set_time = translate_to_timezone(set_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster))
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./3600/24, num=12)
culmination_time = culmination_time[0] if len(culmination_time) > 0 else None
except ValueError:
culmination_time = None

if len(rise_times) == 2:
rise_time = rise_times[0 if arr[0] else 1]
set_time = rise_times[1 if not arr[1] else 0]
else:
rise_time = rise_times[0] if arr[0] else None
set_time = rise_times[0] if not arr[0] else None

# Convert the Time instances to Python datetime objects
if rise_time is not None:
rise_time = translate_to_timezone(rise_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

if culmination_time is not None:
culmination_time = translate_to_timezone(culmination_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

if set_time is not None:
set_time = translate_to_timezone(set_time.utc_datetime().replace(microsecond=0),
to_tz=timezone)

ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster))
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 + 1)
end = datetime.date(end.year, end.month, end.day - 1)

raise OutOfRangeDateError(start, end)


return ephemerides return ephemerides

+ 16
- 5
kosmorrolib/events.py View File

@@ -18,12 +18,14 @@


from datetime import date as date_type from datetime import date as date_type


from skyfield.errors import EphemerisRangeError
from skyfield.timelib import Time from skyfield.timelib import Time
from skyfield.searchlib import find_discrete, find_maxima from skyfield.searchlib import find_discrete, find_maxima
from numpy import pi from numpy import pi


from .data import Event, Star, Planet, ASTERS from .data import Event, Star, Planet, ASTERS
from .dateutil import translate_to_timezone from .dateutil import translate_to_timezone
from .exceptions import OutOfRangeDateError
from .core import get_timescale, get_skf_objects, flatten_list from .core import get_timescale, get_skf_objects, flatten_list




@@ -137,8 +139,17 @@ def search_events(date: date_type, timezone: int = 0) -> [Event]:
start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) start_time = get_timescale().utc(date.year, date.month, date.day, -timezone)
end_time = get_timescale().utc(date.year, date.month, date.day + 1, -timezone) end_time = get_timescale().utc(date.year, date.month, date.day + 1, -timezone)


return sorted(flatten_list([
_search_oppositions(start_time, end_time, timezone),
_search_conjunction(start_time, end_time, timezone),
_search_maximal_elongations(start_time, end_time, timezone)
]), key=lambda event: event.start_time)
try:
return sorted(flatten_list([
_search_oppositions(start_time, end_time, timezone),
_search_conjunction(start_time, end_time, timezone),
_search_maximal_elongations(start_time, end_time, timezone)
]), key=lambda event: event.start_time)
except EphemerisRangeError as error:
start_date = translate_to_timezone(error.start_time.utc_datetime(), timezone)
end_date = translate_to_timezone(error.end_time.utc_datetime(), timezone)

start_date = date_type(start_date.year, start_date.month, start_date.day)
end_date = date_type(end_date.year, end_date.month, end_date.day)

raise OutOfRangeDateError(start_date, end_date)

+ 13
- 0
kosmorrolib/exceptions.py View File

@@ -16,8 +16,21 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.


from datetime import date
from .i18n import _, SHORT_DATE_FORMAT



class UnavailableFeatureError(RuntimeError): class UnavailableFeatureError(RuntimeError):
def __init__(self, msg: str): def __init__(self, msg: str):
super(UnavailableFeatureError, self).__init__() super(UnavailableFeatureError, self).__init__()
self.msg = msg self.msg = msg


class OutOfRangeDateError(RuntimeError):
def __init__(self, min_date: date, max_date: date):
super(OutOfRangeDateError, self).__init__()
self.min_date = min_date
self.max_date = max_date
self.msg = _('The date must be between {minimum_date}'
' and {maximum_date}').format(minimum_date=min_date.strftime(SHORT_DATE_FORMAT),
maximum_date=max_date.strftime(SHORT_DATE_FORMAT))

+ 7
- 0
kosmorrolib/i18n.py View File

@@ -24,6 +24,13 @@ _TRANSLATION = gettext.translation('messages', localedir=_LOCALE_DIR, fallback=T


_ = _TRANSLATION.gettext _ = _TRANSLATION.gettext


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')
SHORT_DATE_FORMAT = _('{month} {day_number}, {year}').format(month='%b', day_number='%d', year='%Y')
TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M')



def ngettext(msgid1, msgid2, number): def ngettext(msgid1, msgid2, number):
# Not using ngettext = _TRANSLATION.ngettext because the linter will give an invalid-name error otherwise # Not using ngettext = _TRANSLATION.ngettext because the linter will give an invalid-name error otherwise


+ 79
- 59
kosmorrolib/locales/messages.pot View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: kosmorro 0.8.0\n" "Project-Id-Version: kosmorro 0.8.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2020-06-02 20:49+0200\n"
"POT-Creation-Date: 2020-06-06 16:43+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -59,107 +59,103 @@ msgstr ""
msgid "Waning crescent" msgid "Waning crescent"
msgstr "" msgstr ""


#: kosmorrolib/data.py:43
#: kosmorrolib/data.py:40
msgid "Unavailable"
msgstr ""

#: kosmorrolib/data.py:44
#, python-format #, python-format
msgid "%s is in opposition" msgid "%s is in opposition"
msgstr "" msgstr ""


#: kosmorrolib/data.py:44
#: kosmorrolib/data.py:45
#, python-format #, python-format
msgid "%s and %s are in conjunction" msgid "%s and %s are in conjunction"
msgstr "" msgstr ""


#: kosmorrolib/data.py:45
#: kosmorrolib/data.py:46
#, python-format #, python-format
msgid "%s occults %s" msgid "%s occults %s"
msgstr "" msgstr ""


#: kosmorrolib/data.py:46
#: kosmorrolib/data.py:47
#, python-format #, python-format
msgid "%s's largest elongation" msgid "%s's largest elongation"
msgstr "" msgstr ""


#: kosmorrolib/data.py:261
#: kosmorrolib/data.py:262
msgid "Sun" msgid "Sun"
msgstr "" msgstr ""


#: kosmorrolib/data.py:262
#: kosmorrolib/data.py:263
msgid "Moon" msgid "Moon"
msgstr "" msgstr ""


#: kosmorrolib/data.py:263
#: kosmorrolib/data.py:264
msgid "Mercury" msgid "Mercury"
msgstr "" msgstr ""


#: kosmorrolib/data.py:264
#: kosmorrolib/data.py:265
msgid "Venus" msgid "Venus"
msgstr "" msgstr ""


#: kosmorrolib/data.py:265
#: kosmorrolib/data.py:266
msgid "Mars" msgid "Mars"
msgstr "" msgstr ""


#: kosmorrolib/data.py:266
#: kosmorrolib/data.py:267
msgid "Jupiter" msgid "Jupiter"
msgstr "" msgstr ""


#: kosmorrolib/data.py:267
#: kosmorrolib/data.py:268
msgid "Saturn" msgid "Saturn"
msgstr "" msgstr ""


#: kosmorrolib/data.py:268
#: kosmorrolib/data.py:269
msgid "Uranus" msgid "Uranus"
msgstr "" msgstr ""


#: kosmorrolib/data.py:269
#: kosmorrolib/data.py:270
msgid "Neptune" msgid "Neptune"
msgstr "" msgstr ""


#: kosmorrolib/data.py:270
#: kosmorrolib/data.py:271
msgid "Pluto" msgid "Pluto"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:36
msgid "{day_of_week} {month} {day_number}, {year}"
msgstr ""

#: kosmorrolib/dumper.py:38
msgid "{month} {day_number}, {hours}:{minutes}"
msgstr ""

#: kosmorrolib/dumper.py:40
msgid "{hours}:{minutes}"
msgstr ""

#: kosmorrolib/dumper.py:120
#: kosmorrolib/dumper.py:117
msgid "Expected events:" msgid "Expected events:"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:124
#: kosmorrolib/dumper.py:121
msgid "Note: All the hours are given in UTC." msgid "Note: All the hours are given in UTC."
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:129
#: kosmorrolib/dumper.py:126
msgid "Note: All the hours are given in the UTC{offset} timezone." msgid "Note: All the hours are given in the UTC{offset} timezone."
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:175 kosmorrolib/dumper.py:255
#: kosmorrolib/dumper.py:172 kosmorrolib/dumper.py:259
msgid "Object" msgid "Object"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:176 kosmorrolib/dumper.py:256
#: kosmorrolib/dumper.py:173 kosmorrolib/dumper.py:260
msgid "Rise time" msgid "Rise time"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:177 kosmorrolib/dumper.py:257
#: kosmorrolib/dumper.py:174 kosmorrolib/dumper.py:261
msgid "Culmination time" msgid "Culmination time"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:178 kosmorrolib/dumper.py:258
#: kosmorrolib/dumper.py:175 kosmorrolib/dumper.py:262
msgid "Set time" msgid "Set time"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:192 kosmorrolib/dumper.py:262
#: kosmorrolib/dumper.py:190
msgid "Moon phase is unavailable for this date."
msgstr ""

#: kosmorrolib/dumper.py:192 kosmorrolib/dumper.py:266
msgid "Moon phase:" msgid "Moon phase:"
msgstr "" msgstr ""


@@ -167,36 +163,36 @@ msgstr ""
msgid "{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}" msgid "{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:242
#: kosmorrolib/dumper.py:246
msgid "A Summary of your Sky" msgid "A Summary of your Sky"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:246
#: kosmorrolib/dumper.py:250
msgid "" msgid ""
"This document summarizes the ephemerides and the events of {date}. It " "This document summarizes the ephemerides and the events of {date}. It "
"aims to help you to prepare your observation session. All the hours are " "aims to help you to prepare your observation session. All the hours are "
"given in {timezone}." "given in {timezone}."
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:252
#: kosmorrolib/dumper.py:256
msgid "" msgid ""
"Don't forget to check the weather forecast before you go out with your " "Don't forget to check the weather forecast before you go out with your "
"equipment." "equipment."
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:254
#: kosmorrolib/dumper.py:258
msgid "Ephemerides of the day" msgid "Ephemerides of the day"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:260
#: kosmorrolib/dumper.py:264
msgid "hours" msgid "hours"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:264
#: kosmorrolib/dumper.py:268
msgid "Expected events" msgid "Expected events"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:378
#: kosmorrolib/dumper.py:382
msgid "" msgid ""
"Building PDFs was not possible, because some dependencies are not " "Building PDFs was not possible, because some dependencies are not "
"installed.\n" "installed.\n"
@@ -204,6 +200,26 @@ msgid ""
"information." "information."
msgstr "" msgstr ""


#: kosmorrolib/exceptions.py:34
msgid "The date must be between {minimum_date} and {maximum_date}"
msgstr ""

#: kosmorrolib/i18n.py:27
msgid "{day_of_week} {month} {day_number}, {year}"
msgstr ""

#: kosmorrolib/i18n.py:29
msgid "{month} {day_number}, {hours}:{minutes}"
msgstr ""

#: kosmorrolib/i18n.py:31
msgid "{month} {day_number}, {year}"
msgstr ""

#: kosmorrolib/i18n.py:32
msgid "{hours}:{minutes}"
msgstr ""

#: kosmorrolib/main.py:61 #: kosmorrolib/main.py:61
msgid "" msgid ""
"Save the planet and paper!\n" "Save the planet and paper!\n"
@@ -217,87 +233,91 @@ msgid ""
"the observation coordinate." "the observation coordinate."
msgstr "" msgstr ""


#: kosmorrolib/main.py:98
#: kosmorrolib/main.py:90
msgid "Could not save the output in \"{path}\": {error}" msgid "Could not save the output in \"{path}\": {error}"
msgstr "" msgstr ""


#: kosmorrolib/main.py:103
#: kosmorrolib/main.py:95
msgid "Selected output format needs an output file (--output)." msgid "Selected output format needs an output file (--output)."
msgstr "" msgstr ""


#: kosmorrolib/main.py:120
#: kosmorrolib/main.py:112
msgid "Moon phase can only be displayed between {min_date} and {max_date}"
msgstr ""

#: kosmorrolib/main.py:134
msgid "Running on Python {python_version}" msgid "Running on Python {python_version}"
msgstr "" msgstr ""


#: kosmorrolib/main.py:126
#: kosmorrolib/main.py:140
msgid "Do you really want to clear Kosmorro's cache? [yN] " msgid "Do you really want to clear Kosmorro's cache? [yN] "
msgstr "" msgstr ""


#: kosmorrolib/main.py:133
#: kosmorrolib/main.py:147
msgid "Answer did not match expected options, cache not cleared." msgid "Answer did not match expected options, cache not cleared."
msgstr "" msgstr ""


#: kosmorrolib/main.py:142
#: kosmorrolib/main.py:156
msgid "" msgid ""
"Compute the ephemerides and the events for a given date, at a given " "Compute the ephemerides and the events for a given date, at a given "
"position on Earth." "position on Earth."
msgstr "" msgstr ""


#: kosmorrolib/main.py:144
#: kosmorrolib/main.py:158
msgid "" msgid ""
"By default, only the events will be computed for today ({date}).\n" "By default, only the events will be computed for today ({date}).\n"
"To compute also the ephemerides, latitude and longitude arguments are " "To compute also the ephemerides, latitude and longitude arguments are "
"needed." "needed."
msgstr "" msgstr ""


#: kosmorrolib/main.py:149
#: kosmorrolib/main.py:163
msgid "Show the program version" msgid "Show the program version"
msgstr "" msgstr ""


#: kosmorrolib/main.py:151
#: kosmorrolib/main.py:165
msgid "Delete all the files Kosmorro stored in the cache." msgid "Delete all the files Kosmorro stored in the cache."
msgstr "" msgstr ""


#: kosmorrolib/main.py:153
#: kosmorrolib/main.py:167
msgid "The format under which the information have to be output" msgid "The format under which the information have to be output"
msgstr "" msgstr ""


#: kosmorrolib/main.py:155
#: kosmorrolib/main.py:169
msgid "" msgid ""
"The observer's latitude on Earth. Can also be set in the " "The observer's latitude on Earth. Can also be set in the "
"KOSMORRO_LATITUDE environment variable." "KOSMORRO_LATITUDE environment variable."
msgstr "" msgstr ""


#: kosmorrolib/main.py:158
#: kosmorrolib/main.py:172
msgid "" msgid ""
"The observer's longitude on Earth. Can also be set in the " "The observer's longitude on Earth. Can also be set in the "
"KOSMORRO_LONGITUDE environment variable." "KOSMORRO_LONGITUDE environment variable."
msgstr "" msgstr ""


#: kosmorrolib/main.py:161
#: kosmorrolib/main.py:175
msgid "" msgid ""
"The date for which the ephemerides must be computed (in the YYYY-MM-DD " "The date for which the ephemerides must be computed (in the YYYY-MM-DD "
"format), or as an interval in the \"[+-]YyMmDd\" format (with Y, M, and D" "format), or as an interval in the \"[+-]YyMmDd\" format (with Y, M, and D"
" numbers). Defaults to the current date ({default_date})" " numbers). Defaults to the current date ({default_date})"
msgstr "" msgstr ""


#: kosmorrolib/main.py:166
#: kosmorrolib/main.py:180
msgid "" msgid ""
"The timezone to display the hours in (e.g. 2 for UTC+2 or -3 for UTC-3). " "The timezone to display the hours in (e.g. 2 for UTC+2 or -3 for UTC-3). "
"Can also be set in the KOSMORRO_TIMEZONE environment variable." "Can also be set in the KOSMORRO_TIMEZONE environment variable."
msgstr "" msgstr ""


#: kosmorrolib/main.py:169
#: kosmorrolib/main.py:183
msgid "Disable the colors in the console." msgid "Disable the colors in the console."
msgstr "" msgstr ""


#: kosmorrolib/main.py:171
#: kosmorrolib/main.py:185
msgid "" msgid ""
"A file to export the output to. If not given, the standard output is " "A file to export the output to. If not given, the standard output is "
"used. This argument is needed for PDF format." "used. This argument is needed for PDF format."
msgstr "" msgstr ""


#: kosmorrolib/main.py:174
#: kosmorrolib/main.py:188
msgid "" msgid ""
"Do not generate a graph to represent the rise and set times in the PDF " "Do not generate a graph to represent the rise and set times in the PDF "
"format." "format."


+ 36
- 22
kosmorrolib/main.py View File

@@ -29,7 +29,7 @@ from . import core
from . import events from . import events


from .data import Position, EARTH from .data import Position, EARTH
from .exceptions import UnavailableFeatureError
from .exceptions import UnavailableFeatureError, OutOfRangeDateError
from .i18n import _ from .i18n import _
from . import ephemerides from . import ephemerides
from .version import VERSION from .version import VERSION
@@ -65,39 +65,31 @@ def main():
print(colored(_("PDF output will not contain the ephemerides, because you didn't provide the observation " print(colored(_("PDF output will not contain the ephemerides, because you didn't provide the observation "
"coordinate."), 'yellow')) "coordinate."), 'yellow'))


try:
timezone = args.timezone

if timezone is None and environment.timezone is not None:
timezone = int(environment.timezone)
elif timezone is None:
timezone = 0

if position is not None:
eph = ephemerides.get_ephemerides(date=compute_date, position=position, timezone=timezone)
else:
eph = None

moon_phase = ephemerides.get_moon_phase(compute_date)
timezone = args.timezone


events_list = events.search_events(compute_date, timezone)
if timezone is None and environment.timezone is not None:
timezone = int(environment.timezone)
elif timezone is None:
timezone = 0


format_dumper = output_formats[output_format](ephemerides=eph, moon_phase=moon_phase, events=events_list,
date=compute_date, timezone=timezone, with_colors=args.colors,
show_graph=args.show_graph)
output = format_dumper.to_string()
try:
output = get_information(compute_date, position, timezone, output_format,
args.colors, args.show_graph)
except UnavailableFeatureError as error: except UnavailableFeatureError as error:
print(colored(error.msg, 'red')) print(colored(error.msg, 'red'))
return 2 return 2
except OutOfRangeDateError as error:
print(colored(error.msg, 'red'))
return 1


if args.output is not None: if args.output is not None:
try: try:
with open(args.output, 'wb') as output_file: with open(args.output, 'wb') as output_file:
output_file.write(output)
output_file.write(output.to_string())
except OSError as error: except OSError as error:
print(_('Could not save the output in "{path}": {error}').format(path=args.output, print(_('Could not save the output in "{path}": {error}').format(path=args.output,
error=error.strerror)) error=error.strerror))
elif not format_dumper.is_file_output_needed():
elif not output.is_file_output_needed():
print(output) print(output)
else: else:
print(colored(_('Selected output format needs an output file (--output).'), color='red')) print(colored(_('Selected output format needs an output file (--output).'), color='red'))
@@ -106,6 +98,28 @@ def main():
return 0 return 0




def get_information(compute_date: date, position: Position, timezone: int,
output_format: str, colors: bool, show_graph: bool) -> dumper.Dumper:
if position is not None:
eph = ephemerides.get_ephemerides(date=compute_date, position=position, timezone=timezone)
else:
eph = None

try:
moon_phase = ephemerides.get_moon_phase(compute_date)
except OutOfRangeDateError as error:
moon_phase = None
print(colored(_('Moon phase can only be displayed'
' between {min_date} and {max_date}').format(min_date=error.min_date,
max_date=error.max_date), 'yellow'))

events_list = events.search_events(compute_date, timezone)

return get_dumpers()[output_format](ephemerides=eph, moon_phase=moon_phase, events=events_list,
date=compute_date, timezone=timezone, with_colors=colors,
show_graph=show_graph)


def get_dumpers() -> {str: dumper.Dumper}: def get_dumpers() -> {str: dumper.Dumper}:
return { return {
'text': dumper.TextDumper, 'text': dumper.TextDumper,


+ 1
- 1
setup.py View File

@@ -42,7 +42,7 @@ setup(
('man/man1', ['manpage/kosmorro.1']), ('man/man1', ['manpage/kosmorro.1']),
('man/man7', ['manpage/kosmorro.7']) ('man/man7', ['manpage/kosmorro.7'])
], ],
install_requires=['skyfield>=1.17.0,<2.0.0', 'tabulate', 'numpy>=1.17.0,<2.0.0', 'termcolor', 'python-dateutil'],
install_requires=['skyfield>=1.21.0,<2.0.0', 'tabulate', 'numpy>=1.17.0,<2.0.0', 'termcolor', 'python-dateutil'],
classifiers=[ classifiers=[
'Development Status :: 3 - Alpha', 'Development Status :: 3 - Alpha',
'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: Linux',


+ 4
- 0
test/dumper.py View File

@@ -269,6 +269,10 @@ class DumperTestCase(unittest.TestCase):
self._get_events(), date=date(2019, 10, 14)).to_string() self._get_events(), date=date(2019, 10, 14)).to_string()
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}')


def test_get_moon_with_moon_phase_none(self):
dumper = TextDumper()
self.assertEqual('Moon phase is unavailable for this date.', dumper.get_moon(None))

@staticmethod @staticmethod
def _get_ephemerides(aster_rise_set=False) -> [AsterEphemerides]: def _get_ephemerides(aster_rise_set=False) -> [AsterEphemerides]:
rise_time = datetime(2019, 10, 14, 8) if aster_rise_set else None rise_time = datetime(2019, 10, 14, 8) if aster_rise_set else None


+ 9
- 0
test/ephemerides.py View File

@@ -3,6 +3,7 @@ from .testutils import expect_assertions
from kosmorrolib import ephemerides from kosmorrolib import ephemerides
from kosmorrolib.data import EARTH, Position, MoonPhase from kosmorrolib.data import EARTH, Position, MoonPhase
from datetime import date from datetime import date
from kosmorrolib.exceptions import OutOfRangeDateError




class EphemeridesTestCase(unittest.TestCase): class EphemeridesTestCase(unittest.TestCase):
@@ -107,6 +108,14 @@ class EphemeridesTestCase(unittest.TestCase):
phase = MoonPhase('WANING_CRESCENT', None, None) phase = MoonPhase('WANING_CRESCENT', None, None)
self.assertEqual('New Moon', phase.get_next_phase_name()) self.assertEqual('New Moon', phase.get_next_phase_name())


def test_get_ephemerides_raises_exception_on_out_of_date_range(self):
with self.assertRaises(OutOfRangeDateError):
ephemerides.get_ephemerides(date(1789, 5, 5), Position(0, 0, EARTH))

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__': if __name__ == '__main__':
unittest.main() unittest.main()

+ 5
- 0
test/events.py View File

@@ -6,6 +6,7 @@ from kosmorrolib import events
from kosmorrolib.data import Event, ASTERS from kosmorrolib.data import Event, ASTERS
from kosmorrolib.core import get_timescale from kosmorrolib.core import get_timescale
from unittest_data_provider import data_provider from unittest_data_provider import data_provider
from kosmorrolib.exceptions import OutOfRangeDateError




class EventTestCase(unittest.TestCase): class EventTestCase(unittest.TestCase):
@@ -61,6 +62,10 @@ class EventTestCase(unittest.TestCase):


self.assertEqual(expected_event.__dict__, actual_event.__dict__) self.assertEqual(expected_event.__dict__, actual_event.__dict__)


def test_get_events_raises_exception_on_out_of_date_range(self):
with self.assertRaises(OutOfRangeDateError):
events.search_events(date(1789, 5, 5))



if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

Loading…
Cancel
Save