Procházet zdrojové kódy

Merge branch 'features'

tags/v0.8.0
Jérôme Deuchnord před 4 roky
rodič
revize
2eaa514323
V databázi nebyl nalezen žádný známý klíč pro tento podpis ID GPG klíče: 72F9D1A7272D53DD
30 změnil soubory, kde provedl 970 přidání a 441 odebrání
  1. +1
    -1
      .github/PULL_REQUEST_TEMPLATE.md
  2. +8
    -2
      .github/workflows/e2e.yml
  3. +1
    -8
      .github/workflows/i18n.yml
  4. +21
    -0
      .github/workflows/pylint.yml
  5. +1
    -4
      .github/workflows/release.yml
  6. +22
    -0
      .github/workflows/unit-tests.yml
  7. +3
    -1
      .gitignore
  8. +6
    -1
      .scripts/tests-e2e.sh
  9. +14
    -3
      Makefile
  10. +1
    -0
      Pipfile
  11. +9
    -1
      Pipfile.lock
  12. +5
    -1
      README.md
  13. +0
    -0
      kosmorro
  14. +115
    -0
      kosmorrolib/assets/pdf/kosmorro.sty
  15. +21
    -45
      kosmorrolib/assets/pdf/template.tex
  16. +31
    -0
      kosmorrolib/core.py
  17. +80
    -34
      kosmorrolib/data.py
  18. +134
    -72
      kosmorrolib/dumper.py
  19. +38
    -83
      kosmorrolib/ephemerides.py
  20. +67
    -55
      kosmorrolib/locales/messages.pot
  21. +24
    -28
      kosmorrolib/main.py
  22. +34
    -0
      manpage/README.md
  23. +4
    -1
      manpage/kosmorro.1.md
  24. +63
    -0
      manpage/kosmorro.7.md
  25. +3
    -2
      setup.py
  26. +1
    -0
      test/__init__.py
  27. +7
    -0
      test/core.py
  28. +171
    -67
      test/dumper.py
  29. +37
    -32
      test/ephemerides.py
  30. +48
    -0
      test/testutils.py

+ 1
- 1
.github/PULL_REQUEST_TEMPLATE.md Zobrazit soubor

@@ -7,7 +7,7 @@
| License | GNU AGPL-v3

**Checklist:**
- [ ] I have updated the manpage
- [ ] I have updated the manpages

<!--
Replace this notice by a short README for your feature/bugfix.


+ 8
- 2
.github/workflows/e2e.yml Zobrazit soubor

@@ -18,11 +18,17 @@ jobs:
run: |
sudo apt install ruby
sudo gem install ronn
pip install -U setuptools pip
cd manpage && ronn kosmorro.1.md && cd ..
pip install -U setuptools pip requests wheel Babel

- name: E2E tests
run: |
export ENVIRONMENT="CI"
bash .scripts/tests-e2e.sh

- name: manpage (section 1)
run: |
man -P $(which cat) kosmorro

- name: manpage (section 7)
run: |
man -P $(which cat) 7 kosmorro

.github/workflows/pythonapp.yml → .github/workflows/i18n.yml Zobrazit soubor

@@ -1,4 +1,4 @@
name: Python application
name: Internationalization check

on: [push, pull_request]

@@ -16,13 +16,6 @@ jobs:
run: |
pip install --upgrade pip pipenv
pipenv sync -d
- name: Unit tests
run: |
pipenv run python -m coverage run -m unittest test
pipenv run codecov --token=${{ secrets.CODECOV_TOKEN }}
- name: Lint
run: |
pipenv run pylint kosmorro *.py kosmorrolib/*.py
- name: Check i18n
run: |
pipenv run python setup.py extract_messages --output-file=/tmp/kosmorro-messages.pot > /dev/null

+ 21
- 0
.github/workflows/pylint.yml Zobrazit soubor

@@ -0,0 +1,21 @@
name: PyLint

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: |
pip install --upgrade pip pipenv
pipenv sync -d
- name: Lint
run: |
pipenv run pylint kosmorro *.py kosmorrolib/*.py

+ 1
- 4
.github/workflows/release.yml Zobrazit soubor

@@ -29,8 +29,5 @@ jobs:
POEDITOR_API_ACCESS: ${{ secrets.POEDITOR_API_ACCESS }}
POEDITOR_PROJECT_ID: 306433
run: |
cd manpage && ronn kosmorro.1.md && cd ..

python .scripts/build/getlangs.py
python setup.py compile_catalog sdist bdist_wheel
make POEDITOR_API_ACCESS="${POEDITOR_API_ACCESS}" POEDITOR_PROJECT_ID="306433" build
twine upload dist/*

+ 22
- 0
.github/workflows/unit-tests.yml Zobrazit soubor

@@ -0,0 +1,22 @@
name: Unit tests

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: |
pip install --upgrade pip pipenv
pipenv sync -d
- name: Unit tests
run: |
pipenv run python -m coverage run -m unittest test
pipenv run codecov --token=${{ secrets.CODECOV_TOKEN }}

+ 3
- 1
.gitignore Zobrazit soubor

@@ -6,8 +6,10 @@ kosmorro.egg-info
coverage.xml
node_modules/
package-lock.json

/kosmorrolib/assets/pdf/*
!/assets/pdf/*.tex
!/kosmorrolib/assets/pdf/*.tex
!/kosmorrolib/assets/pdf/*.sty

/manpage/*
!/manpage/*.md


+ 6
- 1
.scripts/tests-e2e.sh Zobrazit soubor

@@ -73,7 +73,7 @@ echo "==== RUNNING E2E TESTS ===="
echo

# Create the package and install it
assertSuccess "$PYTHON_BIN setup.py sdist"
assertSuccess "make build"
assertSuccess "$PIP_BIN install dist/kosmorro-$VERSION.tar.gz" "CI"

assertSuccess kosmorro
@@ -81,6 +81,10 @@ assertSuccess "kosmorro -h"
assertSuccess "kosmorro -d 2020-01-27"
assertFailure "kosmorro -d yolo-yo-lo"
assertFailure "kosmorro -d 2020-13-32"
assertSuccess "kosmorro --date='+3y 5m3d'"
assertSuccess "kosmorro --date='-1y3d'"
assertFailure "kosmorro --date='+3d4m"
assertFailure "kosmorro -date='3y'"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --timezone=1"
@@ -100,6 +104,7 @@ assertSuccess "$PIP_BIN install latex" "CI"

# Dependencies installed, should not fail
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o /tmp/document.pdf"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o /tmp/document.pdf --no-graph"

# man page
assertSuccess "man --pager=cat kosmorro"


+ 14
- 3
Makefile Zobrazit soubor

@@ -1,3 +1,14 @@
build:
ronn --roff manpage/kosmorro.1.md
ronn --roff manpage/kosmorro.7.md

if [ "$$POEDITOR_API_ACCESS" != "" ]; then \
python3 .scripts/build/getlangs.py; \
python3 setup.py compile_catalog; \
fi

python3 setup.py sdist bdist_wheel

env:
@if [[ "$$RELEASE_NUMBER" == "" ]]; \
then echo "Missing environment variable: RELEASE_NUMBER."; \
@@ -27,9 +38,9 @@ finish-release: env
git add CHANGELOG.md kosmorrolib/version.py kosmorrolib/locales/messages.pot
git commit -m "build: bump version $$RELEASE_NUMBER"
git tag "v$$RELEASE_NUMBER"
git checkout features
git merge master
git checkout master
git checkout features
git merge master
git checkout master

@echo
@echo -e "\e[1mVersion \e[36m$$RELEASE_NUMBER\e[39m successfully tagged!"


+ 1
- 0
Pipfile Zobrazit soubor

@@ -16,6 +16,7 @@ tabulate = "*"
numpy = ">=1.17.0,<2.0.0"
termcolor = "*"
latex = "*"
python-dateutil = "*"

[requires]
python_version = "3"

+ 9
- 1
Pipfile.lock Zobrazit soubor

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "67748949d467fbdca22ccc963430b21be215566f8a49b56ac734b44023ce5ff2"
"sha256": "4bcfaa3140b0b5c03886cf6877b37a1eb9df4e175f43d3ce0c1312496cde8515"
},
"pipfile-spec": 6,
"requires": {
@@ -89,6 +89,14 @@
"index": "pypi",
"version": "==1.18.2"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"index": "pypi",
"version": "==2.8.1"
},
"sgp4": {
"hashes": [
"sha256:0cf5ad876c59d0a5c9fc072b040158cf2efe3cef2610ce7184e1024b74f994b2",


+ 5
- 1
README.md Zobrazit soubor

@@ -67,9 +67,13 @@ Kosmorro can export the computation results to PDF files, but this feature requi

- **A LaTeX distribution:**
- Linux: install TeXLive through your packages manager. Kosmorro just needs the minimal installation, you don't need any extension.
- macOS: install [MacTeX](https://www.tug.org/mactex/)
- macOS: install [MacTeX](https://www.tug.org/mactex/), the basic version will suffice:
- from the official website, choose the _smaller download_
- with Brew: `brew install basictex`
- **The `latex` Python library:**
- Arch Linux: the library is available [on the AUR](https://aur.archlinux.org/packages/python-latex)
- Any other systems: install it through PyPI: `pip install latex`

These dependencies are not installed by default, because they take a lot of place and are not necessary if you are not interested in generating PDF files.
The first time you ask Kosmorro to create a PDF, it may be a little verbose. You can ignore its blahblah.


+ 0
- 0
kosmorro Zobrazit soubor


+ 115
- 0
kosmorrolib/assets/pdf/kosmorro.sty Zobrazit soubor

@@ -0,0 +1,115 @@
%! Package = kosmorro
%! Author = Jérôme Deuchnord
%! Date = 2020-04-26

\NeedsTeXFormat{LaTeX2e}[1994/06/01]
\ProvidesPackage{kosmorro}[2020/04/26 Kosmorro Package]

\RequirePackage{xcolor}
\RequirePackage{fp}

\newcommand{\moonphase}[2]{
\begin{center}
\begin{minipage}{2cm}
\includegraphics[width=\linewidth]{#1}
\end{minipage}
\hspace{5mm}
\begin{minipage}{7cm}
\textbf{\currentmoonphasetitle}\\#2
\end{minipage}
\end{center}
}

\newenvironment{ephemerides}{
\begin{table}[h]
\centering
\begin{tabular}{lccc}
\textbf{\ephemeridesobjecttitle} &
\textbf{\ephemeridesrisetimetitle} &
\textbf{\ephemeridesculminationtimetitle} &
\textbf{\ephemeridessettimetitle}\\
\hline
}{
\end{tabular}
\end{table}
}

\newcommand{\object}[4]{
\hline
\textbf{#1} & {#2} & {#3} & {#4}\\
}

\newenvironment{graphephemerides}{\setlength{\unitlength}{0.02\linewidth}
\begin{picture}(20,20)
% Axes
\put(0,-2){\vector(1,0){50}}
\multiput(0,-2)(2,0){24}{
\line(0,-1){0.25}
}
\newcounter{hour}
\multiput(-0.25,-3.5)(4,0){12}{
\sffamily\footnotesize
\arabic{hour}\stepcounter{hour}\stepcounter{hour}
}
\put(49,-3.5){\sffamily\footnotesize \hourslabel}

% Graduation

\put(50,-0.5){\sffamily\footnotesize \Pluto}
\put(50,1.5){\sffamily\footnotesize \Neptune}
\put(50,3.5){\sffamily\footnotesize \Uranus}
\put(50,5.5){\sffamily\footnotesize \Saturn}
\put(50,7.5){\sffamily\footnotesize \Jupiter}
\put(50,9.5){\sffamily\footnotesize \Mars}
\put(50,11.5){\sffamily\footnotesize \Venus}
\put(50,13.5){\sffamily\footnotesize \Mercury}
\put(50,15.5){\sffamily\footnotesize \Moon}
\put(50,17.5){\sffamily\footnotesize \Sun}

\multiput(0,0)(0,2){10}{
\color{gray}\line(1,0){48}
}

\linethickness{1.5mm}
}{
\end{picture}
\vspace{1cm}
}

\newcommand{\graphobject}[8]{%
% #1: Y coordinate component
% #2: Color
% #3: Hour rise time
% #4: Minute rise time
% #5: Hour set time
% #6: Minute set time
% #7: Human-readable rise time
% #8: Human-readable set time

\FPeval{\start}{#3*2+(#4/60)*2}%
\FPeval{\length}{#5*2+(#6/60)*2 - \start}%
\FPeval{\starttext}{\start+0.7}%
\FPeval{\endtext}{\start+\length-3.25}%

{\color{#2}%
\put(\start,#1){%
\line(1, 0){\length}%
}}%

\put(\starttext,#1.5){\sffamily\footnotesize #7}%
\put(\endtext,#1.5){\sffamily\footnotesize #8}%
}

\newcommand{\event}[2]{
\textbf{#1} & {#2}\\
}

\newenvironment{events}{
\begin{table}[h]
\begin{tabular}{ll}
}{
\end{tabular}
\end{table}
}

\endinput

+ 21
- 45
kosmorrolib/assets/pdf/template.tex Zobrazit soubor

@@ -5,12 +5,31 @@
\usepackage[margin=25mm]{geometry}
\usepackage{graphicx}
\usepackage{hyperref}
\usepackage{kosmorro}

\newcommand{\currentmoonphasetitle}{+++CURRENT-MOON-PHASE-TITLE+++}
\newcommand{\ephemeridesobjecttitle}{+++EPHEMERIDES-OBJECT+++}
\newcommand{\ephemeridesrisetimetitle}{+++EPHEMERIDES-RISE-TIME+++}
\newcommand{\ephemeridesculminationtimetitle}{+++EPHEMERIDES-CULMINATION-TIME+++}
\newcommand{\ephemeridessettimetitle}{+++EPHEMERIDES-SET-TIME+++}
\newcommand{\hourslabel}{+++GRAPH_LABEL_HOURS+++}

\newcommand{\Pluto}{+++ASTER_PLUTO+++}
\newcommand{\Neptune}{+++ASTER_NEPTUNE+++}
\newcommand{\Uranus}{+++ASTER_URANUS+++}
\newcommand{\Saturn}{+++ASTER_SATURN+++}
\newcommand{\Jupiter}{+++ASTER_JUPITER+++}
\newcommand{\Mars}{+++ASTER_MARS+++}
\newcommand{\Venus}{+++ASTER_VENUS+++}
\newcommand{\Mercury}{+++ASTER_MERCURY+++}
\newcommand{\Moon}{+++ASTER_MOON+++}
\newcommand{\Sun}{+++ASTER_SUN+++}

% Fix Unicode issues
\DeclareUnicodeCharacter{202F}{~}
\DeclareUnicodeCharacter{00B0}{$^\circ$}

\hypersetup{pdfinfo={
\hypersetup{pdfinfo={%
Title={+++DOCUMENT-TITLE+++},
Creator={Kosmorro v+++KOSMORRO-VERSION+++}
}}
@@ -23,49 +42,6 @@

\begin{document}

\newcommand{\object}[4]{
\hline
\textbf{#1} & {#2} & {#3} & {#4}\\
}

\newcommand{\moonphase}[2]{
\begin{center}
\begin{minipage}{2cm}
\includegraphics[width=\linewidth]{#1}
\end{minipage}
\hspace{5mm}
\begin{minipage}{7cm}
\textbf{+++CURRENT-MOON-PHASE-TITLE+++}\\#2
\end{minipage}
\end{center}
}

\newenvironment{ephemerides}{
\begin{table}[h]
\centering
\begin{tabular}{lccc}
\textbf{+++EPHEMERIDES-OBJECT+++} &
\textbf{+++EPHEMERIDES-RISE-TIME+++} &
\textbf{+++EPHEMERIDES-CULMINATION-TIME+++} &
\textbf{+++EPHEMERIDES-SET-TIME+++}\\
\hline
}{
\end{tabular}
\end{table}
}

\newcommand{\event}[2]{
\textbf{#1} & {#2}\\
}

\newenvironment{events}{
\begin{table}[h]
\begin{tabular}{ll}
}{
\end{tabular}
\end{table}
}

\maketitle

+++INTRODUCTION+++
@@ -76,7 +52,7 @@
\section{\sffamily +++SECTION-EPHEMERIDES+++}

\begin{ephemerides}
+++EPHEMERIDES+++
+++EPHEMERIDES+++
\end{ephemerides}
%%% END-EPHEMERIDES-SECTION



+ 31
- 0
kosmorrolib/core.py Zobrazit soubor

@@ -21,10 +21,15 @@ import re
from shutil import rmtree
from pathlib import Path

from datetime import date
from dateutil.relativedelta import relativedelta

from skyfield.api import Loader
from skyfield.timelib import Time
from skyfield.nutationlib import iau2000b

from kosmorrolib.i18n import _

CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache'

class Environment:
@@ -86,3 +91,29 @@ def flatten_list(the_list: list):
new_list.append(item)

return new_list


def get_date(date_arg: str) -> date:
if re.match(r'^\d{4}-\d{2}-\d{2}$', date_arg):
try:
return date.fromisoformat(date_arg)
except ValueError as error:
raise ValueError(_('The date {date} is not valid: {error}').format(date=date_arg, error=error.args[0]))
elif re.match(r'^([+-])(([0-9]+)y)?[ ]?(([0-9]+)m)?[ ]?(([0-9]+)d)?$', date_arg):
def get_offset(date_arg: str, signifier: str):
if re.search(r'([0-9]+)' + signifier, date_arg):
return abs(int(re.search(r'[+-]?([0-9]+)' + signifier, date_arg).group(0)[:-1]))
return 0

days = get_offset(date_arg, 'd')
months = get_offset(date_arg, 'm')
years = get_offset(date_arg, 'y')


if date_arg[0] == '+':
return date.today() + relativedelta(days=days, months=months, years=years)
return date.today() - relativedelta(days=days, months=months, years=years)

else:
error_msg = _('The date {date} does not match the required YYYY-MM-DD format or the offset format.')
raise ValueError(error_msg.format(date=date_arg))

+ 80
- 34
kosmorrolib/data.py Zobrazit soubor

@@ -47,7 +47,13 @@ EVENTS = {
}


class MoonPhase:
class Serializable(ABC):
@abstractmethod
def serialize(self) -> dict:
pass


class MoonPhase(Serializable):
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()),
@@ -60,6 +66,11 @@ class MoonPhase:
def get_phase(self):
return MOON_PHASES[self.identifier]

def get_next_phase_name(self):
next_identifier = self.get_next_phase()

return MOON_PHASES[next_identifier]

def get_next_phase(self):
if self.identifier == 'NEW_MOON' or self.identifier == 'WAXING_CRESCENT':
next_identifier = 'FIRST_QUARTER'
@@ -69,39 +80,20 @@ class MoonPhase:
next_identifier = 'LAST_QUARTER'
else:
next_identifier = 'NEW_MOON'
return next_identifier

return MOON_PHASES[next_identifier]


class Position:
def __init__(self, latitude: float, longitude: float):
self.latitude = latitude
self.longitude = longitude
self.observation_planet = None
self._topos = None

def get_planet_topos(self) -> Topos:
if self.observation_planet is None:
raise TypeError('Observation planet must be set.')

if self._topos is None:
self._topos = self.observation_planet + Topos(latitude_degrees=self.latitude,
longitude_degrees=self.longitude)

return self._topos


class AsterEphemerides:
def __init__(self,
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
def serialize(self) -> dict:
return {
'phase': self.identifier,
'time': self.time.isoformat() if self.time is not None else None,
'next': {
'phase': self.get_next_phase(),
'time': self.next_phase_date.isoformat()
}
}


class Object(ABC):
class Object(Serializable):
"""
An astronomical object.
"""
@@ -109,7 +101,6 @@ class Object(ABC):
def __init__(self,
name: str,
skyfield_name: str,
ephemerides: AsterEphemerides or None = None,
radius: float = None):
"""
Initialize an astronomical object
@@ -122,7 +113,6 @@ class Object(ABC):
self.name = name
self.skyfield_name = skyfield_name
self.radius = radius
self.ephemerides = ephemerides

def get_skyfield_object(self) -> SkfPlanet:
return get_skf_objects()[self.skyfield_name]
@@ -143,6 +133,13 @@ class Object(ABC):

return 360 / pi * arcsin(self.radius / from_place.at(time).observe(self.get_skyfield_object()).distance().km)

def serialize(self) -> dict:
return {
'name': self.name,
'type': self.get_type(),
'radius': self.radius,
}


class Star(Object):
def get_type(self) -> str:
@@ -164,7 +161,7 @@ class Satellite(Object):
return 'satellite'


class Event:
class Event(Serializable):
def __init__(self, event_type: str, objects: [Object], start_time: datetime,
end_time: Union[datetime, None] = None, details: str = None):
if event_type not in EVENTS.keys():
@@ -190,6 +187,15 @@ class Event:

return tuple(object.name for object in self.objects)

def serialize(self) -> dict:
return {
'objects': [object.serialize() for object in self.objects],
'event': self.event_type,
'starts_at': self.start_time.isoformat(),
'ends_at': self.end_time.isoformat() if self.end_time is not None else None,
'details': self.details
}


def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonPhase, None]:
tomorrow = get_timescale().utc(now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1)
@@ -228,8 +234,30 @@ def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonP
next_phase_time.utc_datetime() if next_phase_time is not None else None)


class AsterEphemerides(Serializable):
def __init__(self,
rise_time: Union[datetime, None],
culmination_time: Union[datetime, None],
set_time: Union[datetime, None],
aster: Object):
self.rise_time = rise_time
self.culmination_time = culmination_time
self.set_time = set_time
self.object = aster

def serialize(self) -> dict:
return {
'object': self.object.serialize(),
'rise_time': self.rise_time.isoformat() if self.rise_time is not None else None,
'culmination_time': self.culmination_time.isoformat() if self.culmination_time is not None else None,
'set_time': self.set_time.isoformat() if self.set_time is not None else None
}


MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']

EARTH = Planet('Earth', 'EARTH')

ASTERS = [Star(_('Sun'), 'SUN', radius=696342),
Satellite(_('Moon'), 'MOON', radius=1737.4),
Planet(_('Mercury'), 'MERCURY', radius=2439.7),
@@ -240,3 +268,21 @@ ASTERS = [Star(_('Sun'), 'SUN', radius=696342),
Planet(_('Uranus'), 'URANUS BARYCENTER', radius=25559),
Planet(_('Neptune'), 'NEPTUNE BARYCENTER', radius=24764),
Planet(_('Pluto'), 'PLUTO BARYCENTER', radius=1185)]


class Position:
def __init__(self, latitude: float, longitude: float, aster: Object):
self.latitude = latitude
self.longitude = longitude
self.aster = aster
self._topos = None

def get_planet_topos(self) -> Topos:
if self.aster is None:
raise TypeError('Observation planet must be set.')

if self._topos is None:
self._topos = self.aster.get_skyfield_object() + Topos(latitude_degrees=self.latitude,
longitude_degrees=self.longitude)

return self._topos

+ 134
- 72
kosmorrolib/dumper.py Zobrazit soubor

@@ -20,10 +20,11 @@ from abc import ABC, abstractmethod
import datetime
import json
import os
from pathlib import Path
from tabulate import tabulate
from numpy import int64
from termcolor import colored
from .data import Object, AsterEphemerides, MoonPhase, Event
from .data import ASTERS, Object, AsterEphemerides, MoonPhase, Event
from .i18n import _
from .version import VERSION
from .exceptions import UnavailableFeatureError
@@ -40,31 +41,35 @@ 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(), timezone: int = 0,
with_colors: bool = True):
self.ephemeris = ephemeris
def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None,
date: datetime.date = datetime.date.today(), timezone: int = 0, with_colors: bool = True,
show_graph: bool = False):
self.ephemerides = ephemerides
self.moon_phase = moon_phase
self.events = events
self.date = date
self.timezone = timezone
self.with_colors = with_colors
self.show_graph = show_graph

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)
if self.moon_phase.time is not None:
self.moon_phase.time = self._datetime_to_timezone(self.moon_phase.time)
if self.moon_phase.next_phase_date is not None:
self.moon_phase.next_phase_date = self._datetime_to_timezone(
self.moon_phase.next_phase_date)

if self.ephemerides is not None:
for ephemeris in self.ephemerides:
if ephemeris.rise_time is not None:
ephemeris.rise_time = self._datetime_to_timezone(ephemeris.rise_time)
if ephemeris.culmination_time is not None:
ephemeris.culmination_time = self._datetime_to_timezone(ephemeris.culmination_time)
if ephemeris.set_time is not None:
ephemeris.set_time = self._datetime_to_timezone(ephemeris.set_time)

for event in self.events:
event.start_time = self._datetime_to_timezone(event.start_time)
@@ -99,11 +104,11 @@ class Dumper(ABC):

class JsonDumper(Dumper):
def to_string(self):
self.ephemeris['events'] = self.events
self.ephemeris['ephemerides'] = self.ephemeris.pop('details')
return json.dumps(self.ephemeris,
default=self._json_default,
indent=4)
return json.dumps({
'ephemerides': [ephemeris.serialize() for ephemeris in self.ephemerides],
'moon_phase': self.moon_phase.serialize(),
'events': [event.serialize() for event in self.events]
}, indent=4)

@staticmethod
def _json_default(obj):
@@ -139,10 +144,10 @@ class TextDumper(Dumper):
def to_string(self):
text = [self.style(self.get_date_as_string(capitalized=True), 'h1')]

if len(self.ephemeris['details']) > 0:
text.append(self.get_asters(self.ephemeris['details']))
if self.ephemerides is not None:
text.append(self.stringify_ephemerides())

text.append(self.get_moon(self.ephemeris['moon_phase']))
text.append(self.get_moon(self.moon_phase))

if len(self.events) > 0:
text.append('\n'.join([self.style(_('Expected events:'), 'h2'),
@@ -173,28 +178,28 @@ class TextDumper(Dumper):

return styles[tag](text)

def get_asters(self, asters: [Object]) -> str:
def stringify_ephemerides(self) -> str:
data = []

for aster in asters:
name = self.style(aster.name, 'th')
for ephemeris in self.ephemerides:
name = self.style(ephemeris.object.name, 'th')

if aster.ephemerides.rise_time is not None:
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)
if ephemeris.rise_time is not None:
time_fmt = TIME_FORMAT if ephemeris.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT
planet_rise = ephemeris.rise_time.strftime(time_fmt)
else:
planet_rise = '-'

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

if aster.ephemerides.set_time is not None:
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)
if ephemeris.set_time is not None:
time_fmt = TIME_FORMAT if ephemeris.set_time.day == self.date.day else SHORT_DATETIME_FORMAT
planet_set = ephemeris.set_time.strftime(time_fmt)
else:
planet_set = '-'

@@ -219,7 +224,7 @@ class TextDumper(Dumper):
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=moon_phase.get_next_phase_name(),
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)
)
@@ -242,17 +247,28 @@ class _LatexDumper(Dumper):
'assets', 'png', 'kosmorro-logo.png')
moon_phase_graphics = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'assets', 'moonphases', 'png',
'.'.join([self.ephemeris['moon_phase'].identifier.lower().replace('_', '-'),
'.'.join([self.moon_phase.identifier.lower().replace('_', '-'),
'png']))

document = template

if len(self.ephemeris['details']) == 0:
if self.ephemerides is None:
document = self._remove_section(document, 'ephemerides')

if len(self.events) == 0:
document = self._remove_section(document, 'events')

document = self.add_strings(document, kosmorro_logo_path, moon_phase_graphics)

if self.show_graph:
# The graphephemerides environment beginning tag must end with a percent symbol to ensure
# that no extra space will interfere with the graph.
document = document.replace(r'\begin{ephemerides}', r'\begin{graphephemerides}%')\
.replace(r'\end{ephemerides}', r'\end{graphephemerides}')

return document

def add_strings(self, document, kosmorro_logo_path, moon_phase_graphics) -> str:
document = document \
.replace('+++KOSMORRO-VERSION+++', VERSION) \
.replace('+++KOSMORRO-LOGO+++', kosmorro_logo_path) \
@@ -274,41 +290,84 @@ class _LatexDumper(Dumper):
.replace('+++EPHEMERIDES-CULMINATION-TIME+++', _('Culmination time')) \
.replace('+++EPHEMERIDES-SET-TIME+++', _('Set time')) \
.replace('+++EPHEMERIDES+++', self._make_ephemerides()) \
.replace('+++GRAPH_LABEL_HOURS+++', _('hours')) \
.replace('+++MOON-PHASE-GRAPHICS+++', moon_phase_graphics) \
.replace('+++CURRENT-MOON-PHASE-TITLE+++', _('Moon phase:')) \
.replace('+++CURRENT-MOON-PHASE+++', self.ephemeris['moon_phase'].get_phase()) \
.replace('+++CURRENT-MOON-PHASE+++', self.moon_phase.get_phase()) \
.replace('+++SECTION-EVENTS+++', _('Expected events')) \
.replace('+++EVENTS+++', self._make_events())

for aster in ASTERS:
document = document.replace('+++ASTER_%s+++' % aster.skyfield_name.upper().split(' ')[0],
aster.name)

return document

def _make_ephemerides(self) -> str:
latex = []

for aster in self.ephemeris['details']:
if aster.ephemerides.rise_time is not None:
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:
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:
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 = '-'

latex.append(r'\object{%s}{%s}{%s}{%s}' % (aster.name,
aster_rise,
aster_culmination,
aster_set))
graph_y_component = 18

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

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

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

if not self.show_graph:
latex.append(r'\object{%s}{%s}{%s}{%s}' % (ephemeris.object.name,
aster_rise,
aster_culmination,
aster_set))
else:
if ephemeris.rise_time is not None:
raise_hour = ephemeris.rise_time.hour
raise_minute = ephemeris.rise_time.minute
else:
raise_hour = raise_minute = 0
aster_rise = ''

if ephemeris.set_time is not None:
set_hour = ephemeris.set_time.hour
set_minute = ephemeris.set_time.minute
else:
set_hour = 24
set_minute = 0
aster_set = ''
sets_after_end = set_hour > raise_hour

if not sets_after_end:
latex.append(r'\graphobject{%d}{gray}{0}{0}{%d}{%d}{}{%s}' % (graph_y_component,
set_hour,
set_minute,
aster_set))
set_hour = 24
set_minute = 0

latex.append(r'\graphobject{%d}{gray}{%d}{%d}{%d}{%d}{%s}{%s}' % (
graph_y_component,
raise_hour,
raise_minute,
set_hour,
set_minute,
aster_rise,
aster_set if sets_after_end else ''
))
graph_y_component -= 2

return ''.join(latex)

@@ -342,14 +401,15 @@ class _LatexDumper(Dumper):


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 _convert_dates_to_timezones(self):
"""This method is disabled in this dumper, because the timezone is already converted
in :class:`_LatexDumper`."""

def to_string(self):
try:
latex_dumper = _LatexDumper(self.ephemeris, self.events,
date=self.date, timezone=self.timezone, with_colors=self.with_colors)
latex_dumper = _LatexDumper(self.ephemerides, self.moon_phase, self.events,
date=self.date, timezone=self.timezone, with_colors=self.with_colors,
show_graph=self.show_graph)
return self._compile(latex_dumper.to_string())
except RuntimeError:
raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not"
@@ -365,4 +425,6 @@ class PdfDumper(Dumper):
if build_pdf is None:
raise RuntimeError('Python latex module not found')

return bytes(build_pdf(latex_input))
package = str(Path(__file__).parent.absolute()) + '/assets/pdf/'

return bytes(build_pdf(latex_input, [package]))

+ 38
- 83
kosmorrolib/ephemerides.py Zobrazit soubor

@@ -17,77 +17,63 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import datetime
from typing import Union

from skyfield import almanac
from skyfield.searchlib import find_discrete, find_maxima
from skyfield.timelib import Time
from skyfield.constants import tau

from .data import Object, Position, AsterEphemerides, MoonPhase, ASTERS, skyfield_to_moon_phase
from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS, skyfield_to_moon_phase
from .core import get_skf_objects, get_timescale, get_iau2000b

RISEN_ANGLE = -0.8333


class EphemeridesComputer:
def __init__(self, position: Union[Position, None]):
if position is not None:
position.observation_planet = get_skf_objects()['earth']
self.position = position
def get_moon_phase(compute_date: datetime.date) -> MoonPhase:
earth = get_skf_objects()['earth']
moon = get_skf_objects()['moon']
sun = get_skf_objects()['sun']

def get_sun(self, start_time, end_time) -> dict:
times, is_risen = find_discrete(start_time,
end_time,
almanac.sunrise_sunset(get_skf_objects(), self.position))
def moon_phase_at(time: Time):
time._nutation_angles = get_iau2000b(time)
current_earth = earth.at(time)
_, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date')
_, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date')
return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int)

sunrise = times[0] if is_risen[0] else times[1]
sunset = times[1] if not is_risen[1] else times[0]
moon_phase_at.rough_period = 7.0 # one lunar phase per week

return {'rise': sunrise, 'set': sunset}
today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day)
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)

@staticmethod
def get_moon_phase(compute_date: datetime.date) -> MoonPhase:
earth = get_skf_objects()['earth']
moon = get_skf_objects()['moon']
sun = get_skf_objects()['sun']
times, phase = find_discrete(time1, time2, moon_phase_at)

def moon_phase_at(time: Time):
time._nutation_angles = get_iau2000b(time)
current_earth = earth.at(time)
_, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date')
_, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date')
return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int)
return skyfield_to_moon_phase(times, phase, today)

moon_phase_at.rough_period = 7.0 # one lunar phase per week

today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day)
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)
def get_ephemerides(date: datetime.date, position: Position) -> [AsterEphemerides]:
ephemerides = []

times, phase = find_discrete(time1, time2, moon_phase_at)
def get_angle(for_aster: Object):
def fun(time: Time) -> float:
return position.get_planet_topos().at(time).observe(for_aster.get_skyfield_object()).apparent().altaz()[0]\
.degrees
fun.rough_period = 1.0
return fun

return skyfield_to_moon_phase(times, phase, today)
def is_risen(for_aster: Object):
def fun(time: Time) -> bool:
return get_angle(for_aster)(time) > RISEN_ANGLE
fun.rough_period = 0.5
return fun

@staticmethod
def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object:
skyfield_aster = get_skf_objects()[aster.skyfield_name]
start_time = get_timescale().utc(date.year, date.month, date.day)
end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59)

def get_angle(time: Time) -> float:
return position.get_planet_topos().at(time).observe(skyfield_aster).apparent().altaz()[0].degrees

def is_risen(time: Time) -> bool:
return get_angle(time) > RISEN_ANGLE

get_angle.rough_period = 1.0
is_risen.rough_period = 0.5

start_time = get_timescale().utc(date.year, date.month, date.day)
end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59)

rise_times, arr = find_discrete(start_time, end_time, is_risen)
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, epsilon=1./3600/24, num=12)
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
@@ -109,37 +95,6 @@ class EphemeridesComputer:
if set_time is not None:
set_time = set_time.utc_datetime().replace(microsecond=0) if set_time is not None else None

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

@staticmethod
def is_leap_year(year: int) -> bool:
return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0)

def compute_ephemerides(self, compute_date: datetime.date) -> dict:
return {'moon_phase': self.get_moon_phase(compute_date),
'details': [self.get_asters_ephemerides_for_aster(aster, compute_date, self.position)
for aster in ASTERS] if self.position is not None else []}

@staticmethod
def get_seasons(year: int) -> dict:
start_time = get_timescale().utc(year, 1, 1)
end_time = get_timescale().utc(year, 12, 31)
times, almanac_seasons = find_discrete(start_time, end_time, almanac.seasons(get_skf_objects()))

seasons = {}
for time, almanac_season in zip(times, almanac_seasons):
if almanac_season == 0:
season = 'MARCH'
elif almanac_season == 1:
season = 'JUNE'
elif almanac_season == 2:
season = 'SEPTEMBER'
elif almanac_season == 3:
season = 'DECEMBER'
else:
raise AssertionError

seasons[season] = time.utc_iso()

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

return ephemerides

+ 67
- 55
kosmorrolib/locales/messages.pot Zobrazit soubor

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: kosmorro 0.7.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2020-04-05 10:47+0200\n"
"POT-Creation-Date: 2020-05-13 13:11+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,6 +17,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.8.0\n"

#: kosmorrolib/core.py:101
msgid "The date {date} is not valid: {error}"
msgstr ""

#: kosmorrolib/core.py:118
msgid ""
"The date {date} does not match the required YYYY-MM-DD format or the "
"offset format."
msgstr ""

#: kosmorrolib/data.py:32
msgid "New Moon"
msgstr ""
@@ -69,120 +79,124 @@ msgstr ""
msgid "%s's largest elongation"
msgstr ""

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

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

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

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

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

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

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

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

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

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

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

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

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

#: kosmorrolib/dumper.py:148
#: kosmorrolib/dumper.py:153
msgid "Expected events:"
msgstr ""

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

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

#: kosmorrolib/dumper.py:203 kosmorrolib/dumper.py:272
#: kosmorrolib/dumper.py:208 kosmorrolib/dumper.py:288
msgid "Object"
msgstr ""

#: kosmorrolib/dumper.py:204 kosmorrolib/dumper.py:273
#: kosmorrolib/dumper.py:209 kosmorrolib/dumper.py:289
msgid "Rise time"
msgstr ""

#: kosmorrolib/dumper.py:205 kosmorrolib/dumper.py:274
#: kosmorrolib/dumper.py:210 kosmorrolib/dumper.py:290
msgid "Culmination time"
msgstr ""

#: kosmorrolib/dumper.py:206 kosmorrolib/dumper.py:275
#: kosmorrolib/dumper.py:211 kosmorrolib/dumper.py:291
msgid "Set time"
msgstr ""

#: kosmorrolib/dumper.py:220 kosmorrolib/dumper.py:278
#: kosmorrolib/dumper.py:225 kosmorrolib/dumper.py:295
msgid "Moon phase:"
msgstr ""

#: kosmorrolib/dumper.py:221
#: kosmorrolib/dumper.py:226
msgid "{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}"
msgstr ""

#: kosmorrolib/dumper.py:259
#: kosmorrolib/dumper.py:275
msgid "A Summary of your Sky"
msgstr ""

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

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

#: kosmorrolib/dumper.py:271
#: kosmorrolib/dumper.py:287
msgid "Ephemerides of the day"
msgstr ""

#: kosmorrolib/dumper.py:280
#: kosmorrolib/dumper.py:293
msgid "hours"
msgstr ""

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

#: kosmorrolib/dumper.py:355
#: kosmorrolib/dumper.py:415
msgid ""
"Building PDFs was not possible, because some dependencies are not "
"installed.\n"
@@ -190,103 +204,101 @@ msgid ""
"information."
msgstr ""

#: kosmorrolib/main.py:58
#: kosmorrolib/main.py:61
msgid ""
"Save the planet and paper!\n"
"Consider printing you PDF document only if really necessary, and use the "
"other side of the sheet."
msgstr ""

#: kosmorrolib/main.py:62
#: kosmorrolib/main.py:65
msgid ""
"PDF output will not contain the ephemerides, because you didn't provide "
"the observation coordinate."
msgstr ""

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

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

#: kosmorrolib/main.py:104
msgid "The date {date} does not match the required YYYY-MM-DD format."
msgstr ""

#: kosmorrolib/main.py:109
msgid "The date {date} is not valid: {error}"
msgstr ""

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

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

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

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

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

#: kosmorrolib/main.py:152
#: kosmorrolib/main.py:145
msgid "Show the program version"
msgstr ""

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

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

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

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

#: kosmorrolib/main.py:164
#: kosmorrolib/main.py:157
msgid ""
"The date for which the ephemerides must be computed (in the YYYY-MM-DD "
"format). Defaults to the current date ({default_date})"
msgstr ""

#: kosmorrolib/main.py:168
#: kosmorrolib/main.py:161
msgid ""
"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."
msgstr ""

#: kosmorrolib/main.py:171
#: kosmorrolib/main.py:164
msgid "Disable the colors in the console."
msgstr ""

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

#: kosmorrolib/main.py:169
msgid ""
"Generate a graph instead of a table to show the rise, culmination set "
"times (PDF only)"
msgstr ""


+ 24
- 28
kosmorrolib/main.py Zobrazit soubor

@@ -24,25 +24,28 @@ import sys
from datetime import date
from termcolor import colored

from kosmorrolib.version import VERSION
from kosmorrolib import dumper
from kosmorrolib import core
from kosmorrolib import events
from kosmorrolib.i18n import _
from .ephemerides import EphemeridesComputer, Position
from . import dumper
from . import core
from . import events

from .data import Position, EARTH
from .exceptions import UnavailableFeatureError
from .i18n import _
from . import ephemerides
from .version import VERSION


def main():
environment = core.get_env()
output_formats = get_dumpers()
args = get_args(list(output_formats.keys()))
output_format = args.format

if args.special_action is not None:
return 0 if args.special_action() else 1

try:
compute_date = get_date(args.date)
compute_date = core.get_date(args.date)
except ValueError as error:
print(colored(error.args[0], color='red', attrs=['bold']))
return -1
@@ -50,11 +53,11 @@ def main():
position = None

if args.latitude is not None or args.longitude is not None:
position = Position(args.latitude, args.longitude)
position = Position(args.latitude, args.longitude, EARTH)
elif environment.latitude is not None and environment.longitude is not None:
position = Position(float(environment.latitude), float(environment.longitude))
position = Position(float(environment.latitude), float(environment.longitude), EARTH)

if args.format == 'pdf':
if output_format == 'pdf':
print(_('Save the planet and paper!\n'
'Consider printing you PDF document only if really necessary, and use the other side of the sheet.'))
if position is None:
@@ -63,8 +66,8 @@ def main():
"coordinate."), 'yellow'))

try:
ephemeris = EphemeridesComputer(position)
ephemerides = ephemeris.compute_ephemerides(compute_date)
eph = ephemerides.get_ephemerides(date=compute_date, position=position) if position is not None else None
moon_phase = ephemerides.get_moon_phase(compute_date)

events_list = events.search_events(compute_date)

@@ -75,10 +78,10 @@ def main():
elif timezone is None:
timezone = 0

selected_dumper = output_formats[args.format](ephemerides, events_list,
date=compute_date, timezone=timezone,
with_colors=args.colors)
output = selected_dumper.to_string()
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()
except UnavailableFeatureError as error:
print(colored(error.msg, 'red'))
return 2
@@ -90,7 +93,7 @@ def main():
except OSError as error:
print(_('Could not save the output in "{path}": {error}').format(path=args.output,
error=error.strerror))
elif not selected_dumper.is_file_output_needed():
elif not format_dumper.is_file_output_needed():
print(output)
else:
print(colored(_('Selected output format needs an output file (--output).'), color='red'))
@@ -99,21 +102,11 @@ def main():
return 0


def get_date(yyyymmdd: str) -> date:
if not re.match(r'^\d{4}-\d{2}-\d{2}$', yyyymmdd):
raise ValueError(_('The date {date} does not match the required YYYY-MM-DD format.').format(date=yyyymmdd))

try:
return date.fromisoformat(yyyymmdd)
except ValueError as error:
raise ValueError(_('The date {date} is not valid: {error}').format(date=yyyymmdd, error=error.args[0]))


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


@@ -172,5 +165,8 @@ def get_args(output_formats: [str]):
parser.add_argument('--output', '-o', type=str, default=None,
help=_('A file to export the output to. If not given, the standard output is used. '
'This argument is needed for PDF format.'))
parser.add_argument('--no-graph', dest='show_graph', action='store_false',
help=_('Generate a graph instead of a table to show the rise, culmination set times '
'(PDF only)'))

return parser.parse_args()

+ 34
- 0
manpage/README.md Zobrazit soubor

@@ -0,0 +1,34 @@
# Kosmorro's manpages

This folder contains Kosmorro's manpages.

Two sections are available:

- Section 1: contains the details about the command line usage.
- Section 7: contains the vocabulary used in Kosmorro along with their definitions.

## How to use it

To open the manpage from section 1, open a terminal and invoke:

```bash
man kosmorro
```

If you want to open the vocabulary:

```bash
man 7 kosmorro
````

## `man` complains there's "No manual entry for kosmorro"

Sometimes, especially on Mac, `man` needs to be informed about where the manpages are stored by Python 3. Invoke the following command to do this:

```bash
echo 'export MANPATH=/usr/local/man:$MANPATH' >> $HOME/.bashrc
```

And open a new terminal.

NB: if you are not using Bash, change `.bashrc` with the correct file.

+ 4
- 1
manpage/kosmorro.1.md Zobrazit soubor

@@ -23,7 +23,7 @@
the observer's longitude on Earth

`--date=`_DATE_, `-d` _DATE_
The date for which the ephemerides must be computed (in the YYYY-MM-DD format); defaults to the current date
The date for which the ephemerides must be computed, either in the YYYY-MM-DD format or as an interval in the "[+-]YyMmDd" format (with Y, M, and D numbers); defaults to the current date

`--timezone=`_TIMEZONE_, `-t` _TIMEZONE_
the timezone to display the hours in; e.g. 2 for UTC+2 or -3 for UTC-3
@@ -37,6 +37,9 @@
`--format=`_FORMAT_, `-f` _FORMAT_
the format under which the information have to be output; one of the following: text, json, pdf

`--no-graph`
present the ephemerides in a table instead of a graph; PDF output format only

## ENVIRONMENT VARIABLES

The environment variable listed below may be used instead of the options.


+ 63
- 0
manpage/kosmorro.7.md Zobrazit soubor

@@ -0,0 +1,63 @@
# kosmorro(7) -- a program that computes the ephemerides

## DESCRIPTION

This manual explains the different terms that one can find when using **kosmorro**(1).
The terms are given in an alphabetically order.

## TERMS

### Conjunction

From the point of view of the Earth, two asters are said in conjunction when they are close together.
It is, of course, an illusion caused by the position of the Earth combined with the two other objects' ones.

### Elongation

The elongation is the angle of visual separation between a planet and the Sun, as seen from the point of view of the Earth.
For the inferior planets, the time when the elongation is maximal is propice to their observation, because it is the moment when they are the most visible.

### Occultation

An occultation is a special kind of conjunction where the closer aster to the Earth hides, at least partially, another one.

### Opposition

An aster is said in opposition when it is positionned at the exact opposite of the Sun, from the point of view of the Earth, i.e. when their angle is equal to 180 degrees.
For all the superior planets, it is the best moment to observe them, because they will be at the smallest distance to the Earth. Plus, they will appear full.
For instance, Mars is in opposition when the angle Mars-Earth-Sun is equal to 180 degrees.

### Planet

A planet is an aster that orbits around a star, is not a star itself, is massive enough to maintain it nearly round, and has cleaned its orbit from other massive objects.

**Inferior planet**

A planet is said "inferior" if its orbit radius is smaller than the planet of reference.
For instance, the inferior planets from the point of view of the Earth are Mercury and Venus.

The term should not be confused with "inner planet".

**Inner planet**

The "inner planet" term refers to the planets orbiting below the orbit of the asteroid belt between Mars and Jupiter.

**Outer planet**

The "outer planet" term refers to the planets orbiting beyond the orbit of the asteroid belt between Mars and Jupiter.

**Superior planet**

A planet is said "superior" if its orbit radius is higher than the planet of reference.
For instance, the superior planets from the point of view of the Earth are Mars, Jupiter, Saturn, Uranus and Neptune.

The term should not be confused with "outer planet".

## AUTHOR

Written by Jérôme Deuchnord.

## COPYRIGHT

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.


+ 3
- 2
setup.py Zobrazit soubor

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


+ 1
- 0
test/__init__.py Zobrazit soubor

@@ -3,3 +3,4 @@ from .data import *
from .dumper import *
from .ephemerides import *
from .events import *
from .testutils import *

+ 7
- 0
test/core.py Zobrazit soubor

@@ -3,6 +3,9 @@ import unittest
import os
import kosmorrolib.core as core

from datetime import date
from dateutil.relativedelta import relativedelta


class CoreTestCase(unittest.TestCase):
def test_flatten_list(self):
@@ -27,6 +30,10 @@ class CoreTestCase(unittest.TestCase):

self.assertEqual("{'great_variable': 'value', 'another_variable': 'another value'}", str(env))

def test_date_arg_parsing(self):
self.assertEqual(core.get_date("+1y 2m3d"), date.today() + relativedelta(years=1, months=2, days=3))
self.assertEqual(core.get_date("-1y2d"), date.today() - relativedelta(years=1, days=2))
self.assertEqual(core.get_date("1111-11-13"), date(1111, 11, 13))

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

+ 171
- 67
test/dumper.py Zobrazit soubor

@@ -11,86 +11,110 @@ class DumperTestCase(unittest.TestCase):

def test_json_dumper_returns_correct_json(self):
self.assertEqual('{\n'
' "ephemerides": [\n'
' {\n'
' "object": {\n'
' "name": "Mars",\n'
' "type": "planet",\n'
' "radius": null\n'
' },\n'
' "rise_time": null,\n'
' "culmination_time": null,\n'
' "set_time": null\n'
' }\n'
' ],\n'
' "moon_phase": {\n'
' "next_phase_date": "2019-10-21T00:00:00",\n'
' "phase": "FULL_MOON",\n'
' "date": "2019-10-14T00:00:00"\n'
' "time": "2019-10-14T00:00:00",\n'
' "next": {\n'
' "phase": "LAST_QUARTER",\n'
' "time": "2019-10-21T00:00:00"\n'
' }\n'
' },\n'
' "events": [\n'
' {\n'
' "event_type": "OPPOSITION",\n'
' "objects": [\n'
' "Mars"\n'
' {\n'
' "name": "Mars",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n'
' "start_time": "2019-10-14T23:00:00",\n'
' "end_time": null,\n'
' "event": "OPPOSITION",\n'
' "starts_at": "2019-10-14T23:00:00",\n'
' "ends_at": null,\n'
' "details": null\n'
' },\n'
' {\n'
' "event_type": "MAXIMAL_ELONGATION",\n'
' "objects": [\n'
' "Venus"\n'
' {\n'
' "name": "Venus",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n'
' "start_time": "2019-10-14T12:00:00",\n'
' "end_time": null,\n'
' "event": "MAXIMAL_ELONGATION",\n'
' "starts_at": "2019-10-14T12:00:00",\n'
' "ends_at": null,\n'
' "details": "42.0\\u00b0"\n'
' }\n'
' ],\n'
' "ephemerides": [\n'
' {\n'
' "object": "Mars",\n'
' "details": {\n'
' "rise_time": null,\n'
' "culmination_time": null,\n'
' "set_time": null\n'
' }\n'
' }\n'
' ]\n'
'}', JsonDumper(self._get_data(), self._get_events()).to_string())
'}', JsonDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events()).to_string())

data = self._get_data(aster_rise_set=True)
self.assertEqual('{\n'
' "ephemerides": [\n'
' {\n'
' "object": {\n'
' "name": "Mars",\n'
' "type": "planet",\n'
' "radius": null\n'
' },\n'
' "rise_time": "2019-10-14T08:00:00",\n'
' "culmination_time": "2019-10-14T13:00:00",\n'
' "set_time": "2019-10-14T23:00:00"\n'
' }\n'
' ],\n'
' "moon_phase": {\n'
' "next_phase_date": "2019-10-21T00:00:00",\n'
' "phase": "FULL_MOON",\n'
' "date": "2019-10-14T00:00:00"\n'
' "time": "2019-10-14T00:00:00",\n'
' "next": {\n'
' "phase": "LAST_QUARTER",\n'
' "time": "2019-10-21T00:00:00"\n'
' }\n'
' },\n'
' "events": [\n'
' {\n'
' "event_type": "OPPOSITION",\n'
' "objects": [\n'
' "Mars"\n'
' {\n'
' "name": "Mars",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n'
' "start_time": "2019-10-14T23:00:00",\n'
' "end_time": null,\n'
' "event": "OPPOSITION",\n'
' "starts_at": "2019-10-14T23:00:00",\n'
' "ends_at": null,\n'
' "details": null\n'
' },\n'
' {\n'
' "event_type": "MAXIMAL_ELONGATION",\n'
' "objects": [\n'
' "Venus"\n'
' {\n'
' "name": "Venus",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n'
' "start_time": "2019-10-14T12:00:00",\n'
' "end_time": null,\n'
' "event": "MAXIMAL_ELONGATION",\n'
' "starts_at": "2019-10-14T12:00:00",\n'
' "ends_at": null,\n'
' "details": "42.0\\u00b0"\n'
' }\n'
' ],\n'
' "ephemerides": [\n'
' {\n'
' "object": "Mars",\n'
' "details": {\n'
' "rise_time": "2019-10-14T08:00:00",\n'
' "culmination_time": "2019-10-14T13:00:00",\n'
' "set_time": "2019-10-14T23:00:00"\n'
' }\n'
' }\n'
' ]\n'
'}', JsonDumper(data,
self._get_events()
).to_string())
'}', JsonDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(),
self._get_events()).to_string())

def test_text_dumper_without_events(self):
ephemerides = self._get_data()
ephemerides = self._get_ephemerides()
self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n'
@@ -98,9 +122,9 @@ class DumperTestCase(unittest.TestCase):
'Moon phase: Full Moon\n'
'Last Quarter on Monday October 21, 2019 at 00:00\n\n'
'Note: All the hours are given in UTC.',
TextDumper(ephemerides, [], date=date(2019, 10, 14), with_colors=False).to_string())
TextDumper(ephemerides, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string())

ephemerides = self._get_data(aster_rise_set=True)
ephemerides = self._get_ephemerides(aster_rise_set=True)
self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n'
@@ -108,10 +132,10 @@ class DumperTestCase(unittest.TestCase):
'Moon phase: Full Moon\n'
'Last Quarter on Monday October 21, 2019 at 00:00\n\n'
'Note: All the hours are given in UTC.',
TextDumper(ephemerides, [], date=date(2019, 10, 14), with_colors=False).to_string())
TextDumper(ephemerides, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string())

def test_text_dumper_with_events(self):
ephemerides = self._get_data()
ephemerides = self._get_ephemerides()
self.assertEqual("Monday October 14, 2019\n\n"
"Object Rise time Culmination time Set time\n"
"-------- ----------- ------------------ ----------\n"
@@ -122,10 +146,9 @@ class DumperTestCase(unittest.TestCase):
"23:00 Mars is in opposition\n"
"12:00 Venus's largest elongation (42.0°)\n\n"
"Note: All the hours are given in UTC.",
TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string())
TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string())

def test_text_dumper_without_ephemerides_and_with_events(self):
ephemerides = self._get_data(False)
self.assertEqual('Monday October 14, 2019\n\n'
'Moon phase: Full Moon\n'
'Last Quarter on Monday October 21, 2019 at 00:00\n\n'
@@ -133,10 +156,12 @@ class DumperTestCase(unittest.TestCase):
'23:00 Mars is in opposition\n'
"12:00 Venus's largest elongation (42.0°)\n\n"
'Note: All the hours are given in UTC.',
TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string())
TextDumper(None, self._get_moon_phase(), self._get_events(),
date=date(2019, 10, 14), with_colors=False).to_string())

def test_timezone_is_taken_in_account(self):
ephemerides = self._get_data(aster_rise_set=True)
ephemerides = self._get_ephemerides(aster_rise_set=True)

self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ -------------\n'
@@ -147,9 +172,11 @@ class DumperTestCase(unittest.TestCase):
'Oct 15, 00:00 Mars is in opposition\n'
"13:00 Venus's largest elongation (42.0°)\n\n"
'Note: All the hours are given in the UTC+1 timezone.',
TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False, timezone=1).to_string())
TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14),
with_colors=False, timezone=1).to_string())

ephemerides = self._get_ephemerides(aster_rise_set=True)

ephemerides = self._get_data(aster_rise_set=True)
self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n'
@@ -160,10 +187,13 @@ class DumperTestCase(unittest.TestCase):
'22:00 Mars is in opposition\n'
"11:00 Venus's largest elongation (42.0°)\n\n"
'Note: All the hours are given in the UTC-1 timezone.',
TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False, timezone=-1).to_string())
TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14),
with_colors=False, timezone=-1).to_string())

def test_latex_dumper(self):
latex = _LatexDumper(self._get_data(), self._get_events(), date=date(2019, 10, 14)).to_string()
latex = _LatexDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events(),
date=date(2019, 10, 14)).to_string()

self.assertRegex(latex, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\section{\\sffamily Expected events}')
@@ -172,12 +202,14 @@ class DumperTestCase(unittest.TestCase):
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}')
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}")

latex = _LatexDumper(self._get_data(aster_rise_set=True),
latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(),
self._get_events(), date=date(2019, 10, 14)).to_string()
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}')

def test_latex_dumper_without_ephemerides(self):
latex = _LatexDumper(self._get_data(False), self._get_events(), date=date(2019, 10, 14)).to_string()
latex = _LatexDumper(None, self._get_moon_phase(), self._get_events(),
date=date(2019, 10, 14)).to_string()

self.assertRegex(latex, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\section{\\sffamily Expected events}')
@@ -188,7 +220,8 @@ class DumperTestCase(unittest.TestCase):
self.assertNotRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')

def test_latex_dumper_without_events(self):
latex = _LatexDumper(self._get_data(), [], date=date(2019, 10, 14)).to_string()
latex = _LatexDumper(self._get_ephemerides(), self._get_moon_phase(), [], date=date(2019, 10, 14)).to_string()

self.assertRegex(latex, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\object\{Mars\}\{-\}\{-\}\{-\}')
@@ -196,17 +229,88 @@ class DumperTestCase(unittest.TestCase):

self.assertNotRegex(latex, r'\\section{\\sffamily Expected events}')

def test_latex_dumper_with_graph(self):
latex = _LatexDumper(self._get_ephemerides(True), self._get_moon_phase(), self._get_events(),
date=date(2019, 10, 14), show_graph=True).to_string()

self.assertRegex(latex, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\section{\\sffamily Expected events}')
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{8\}\{0\}\{23\}\{0\}\{08:00\}\{23:00\}')
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}')
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}")

latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(),
self._get_events(), date=date(2019, 10, 14)).to_string()
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}')

def test_latex_dumper_with_graph_but_without_rise_time(self):
ephemerides = self._get_ephemerides(True)
ephemerides[0].rise_time = None
latex = _LatexDumper(ephemerides, self._get_moon_phase(), self._get_events(),
date=date(2019, 10, 14), show_graph=True).to_string()

self.assertRegex(latex, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\section{\\sffamily Expected events}')
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{0\}\{0\}\{23\}\{0\}\{\}\{23:00\}')
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}')
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}")

latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(),
self._get_events(), date=date(2019, 10, 14)).to_string()
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}')

def test_latex_dumper_with_graph_but_without_set_time(self):
ephemerides = self._get_ephemerides(True)
ephemerides[0].set_time = None
latex = _LatexDumper(ephemerides, self._get_moon_phase(), self._get_events(),
date=date(2019, 10, 14), show_graph=True).to_string()

self.assertRegex(latex, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\section{\\sffamily Expected events}')
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{8\}\{0\}\{24\}\{0\}\{08:00\}\{\}')
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}')
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}")

latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(),
self._get_events(), date=date(2019, 10, 14)).to_string()
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}')

def test_latex_dumper_with_graph_but_mars_sets_tomorrow(self):
ephemerides = self._get_ephemerides(True)
ephemerides[0].set_time = datetime(2019, 10, 15, 1)
latex = _LatexDumper(ephemerides, self._get_moon_phase(), self._get_events(),
date=date(2019, 10, 14), show_graph=True).to_string()

self.assertRegex(latex, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\section{\\sffamily Expected events}')
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{8\}\{0\}\{24\}\{0\}\{08:00\}\{\}')
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{0\}\{0\}\{1\}\{0\}\{\}\{Oct 15, 01:00\}')
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}')
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}")

latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(),
self._get_events(), date=date(2019, 10, 14)).to_string()
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}')

@staticmethod
def _get_data(has_ephemerides: bool = True, aster_rise_set=False):
def _get_ephemerides(aster_rise_set=False) -> [AsterEphemerides]:
rise_time = datetime(2019, 10, 14, 8) if aster_rise_set else None
culmination_time = datetime(2019, 10, 14, 13) if aster_rise_set else None
set_time = datetime(2019, 10, 14, 23) if aster_rise_set else None

return {
'moon_phase': MoonPhase('FULL_MOON', datetime(2019, 10, 14), datetime(2019, 10, 21)),
'details': [Planet('Mars', 'MARS',
AsterEphemerides(rise_time, culmination_time, set_time))] if has_ephemerides else []
}
return [AsterEphemerides(rise_time, culmination_time, set_time, Planet('Mars', 'MARS'))]
@staticmethod
def _get_moon_phase():
return MoonPhase('FULL_MOON', datetime(2019, 10, 14), datetime(2019, 10, 21))

@staticmethod
def _get_events():


+ 37
- 32
test/ephemerides.py Zobrazit soubor

@@ -1,106 +1,111 @@
import unittest
from kosmorrolib.ephemerides import EphemeridesComputer
from kosmorrolib.core import get_skf_objects
from kosmorrolib.data import Star, Position, MoonPhase
from .testutils import expect_assertions
from kosmorrolib import ephemerides
from kosmorrolib.data import EARTH, Position, MoonPhase
from datetime import date


class EphemeridesComputerTestCase(unittest.TestCase):
class EphemeridesTestCase(unittest.TestCase):
def test_get_ephemerides_for_aster_returns_correct_hours(self):
position = Position(0, 0)
position.observation_planet = get_skf_objects()['earth']
star = EphemeridesComputer.get_asters_ephemerides_for_aster(Star('Sun', skyfield_name='sun'),
date=date(2019, 11, 18),
position=position)
position = Position(0, 0, EARTH)
eph = ephemerides.get_ephemerides(date=date(2019, 11, 18),
position=position)

self.assertRegex(star.ephemerides.rise_time.isoformat(), '^2019-11-18T05:41:')
self.assertRegex(star.ephemerides.culmination_time.isoformat(), '^2019-11-18T11:45:')
self.assertRegex(star.ephemerides.set_time.isoformat(), '^2019-11-18T17:48:')
@expect_assertions(self.assertRegex, num=3)
def do_assertions(assert_regex):
for ephemeris in eph:
if ephemeris.object.skyfield_name == 'SUN':
assert_regex(ephemeris.rise_time.isoformat(), '^2019-11-18T05:41:')
assert_regex(ephemeris.culmination_time.isoformat(), '^2019-11-18T11:45:')
assert_regex(ephemeris.set_time.isoformat(), '^2019-11-18T17:48:')
break

do_assertions()

###################################################################################################################
### MOON PHASE TESTS ###
###################################################################################################################

def test_moon_phase_new_moon(self):
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 25))
phase = ephemerides.get_moon_phase(date(2019, 11, 25))
self.assertEqual('WANING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T')

phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 26))
phase = ephemerides.get_moon_phase(date(2019, 11, 26))
self.assertEqual('NEW_MOON', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T')

phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 27))
phase = ephemerides.get_moon_phase(date(2019, 11, 27))
self.assertEqual('WAXING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T')

def test_moon_phase_first_crescent(self):
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 3))
phase = ephemerides.get_moon_phase(date(2019, 11, 3))
self.assertEqual('WAXING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-04T')

phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 4))
phase = ephemerides.get_moon_phase(date(2019, 11, 4))
self.assertEqual('FIRST_QUARTER', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T')

phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 5))
phase = ephemerides.get_moon_phase(date(2019, 11, 5))
self.assertEqual('WAXING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T')

def test_moon_phase_full_moon(self):
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 11))
phase = ephemerides.get_moon_phase(date(2019, 11, 11))
self.assertEqual('WAXING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T')

phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 12))
phase = ephemerides.get_moon_phase(date(2019, 11, 12))
self.assertEqual('FULL_MOON', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T')

phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 13))
phase = ephemerides.get_moon_phase(date(2019, 11, 13))
self.assertEqual('WANING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T')

def test_moon_phase_last_quarter(self):
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 18))
phase = ephemerides.get_moon_phase(date(2019, 11, 18))
self.assertEqual('WANING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T')

phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 19))
phase = ephemerides.get_moon_phase(date(2019, 11, 19))
self.assertEqual('LAST_QUARTER', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T')

phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 20))
phase = ephemerides.get_moon_phase(date(2019, 11, 20))
self.assertEqual('WANING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T')

def test_moon_phase_prediction(self):
phase = MoonPhase('NEW_MOON', None, None)
self.assertEqual('First Quarter', phase.get_next_phase())
self.assertEqual('First Quarter', phase.get_next_phase_name())
phase = MoonPhase('WAXING_CRESCENT', None, None)
self.assertEqual('First Quarter', phase.get_next_phase())
self.assertEqual('First Quarter', phase.get_next_phase_name())

phase = MoonPhase('FIRST_QUARTER', None, None)
self.assertEqual('Full Moon', phase.get_next_phase())
self.assertEqual('Full Moon', phase.get_next_phase_name())
phase = MoonPhase('WAXING_GIBBOUS', None, None)
self.assertEqual('Full Moon', phase.get_next_phase())
self.assertEqual('Full Moon', phase.get_next_phase_name())

phase = MoonPhase('FULL_MOON', None, None)
self.assertEqual('Last Quarter', phase.get_next_phase())
self.assertEqual('Last Quarter', phase.get_next_phase_name())
phase = MoonPhase('WANING_GIBBOUS', None, None)
self.assertEqual('Last Quarter', phase.get_next_phase())
self.assertEqual('Last Quarter', phase.get_next_phase_name())

phase = MoonPhase('LAST_QUARTER', None, None)
self.assertEqual('New Moon', phase.get_next_phase())
self.assertEqual('New Moon', phase.get_next_phase_name())
phase = MoonPhase('WANING_CRESCENT', None, None)
self.assertEqual('New Moon', phase.get_next_phase())
self.assertEqual('New Moon', phase.get_next_phase_name())


if __name__ == '__main__':


+ 48
- 0
test/testutils.py Zobrazit soubor

@@ -0,0 +1,48 @@
import functools
from unittest import mock


def expect_assertions(assert_fun, num=1):
"""Asserts that an assertion function is called as expected.

This is very useful when the assertions are in loops.
To use it, create a nested function in the the test function.
The nested function will receive as parameter the mocked assertion function to use in place of the original one.
Finally, run the nested function.

Example of use:

>>> # the function we test:
>>> def my_sum_function(n, m):
>>> # some code here
>>> pass

>>> # The unit test:
>>> def test_sum(self):
>>> @expect_assertions(self.assertEqual, num=10):
>>> def make_assertions(assert_equal):
>>> for i in range (-5, 5):
>>> for j in range(-5, 5):
>>> assert_equal(i + j, my_sum_function(i, j)
>>>
>>> make_assertions() # You don't need to give any parameter, the decorator does it for you.

:param assert_fun: the assertion function to test
:param num: the number of times the assertion function is expected to be called
"""
assert_fun_mock = mock.Mock(side_effect=assert_fun)

def fun_decorator(fun):
@functools.wraps(fun)
def sniff_function():
fun(assert_fun_mock)

count = assert_fun_mock.call_count
if count != num:
raise AssertionError('Expected %d call(s) to function %s but called %d time(s).' % (num,
assert_fun.__name__,
count))

return sniff_function

return fun_decorator

Načítá se…
Zrušit
Uložit