Просмотр исходного кода

Merge branch 'features'

tags/v0.8.0
Jérôme Deuchnord 4 лет назад
Родитель
Сommit
2eaa514323
Не найден GPG ключ соответствующий данной подписи Идентификатор GPG ключа: 72F9D1A7272D53DD
30 измененных файлов: 970 добавлений и 441 удалений
  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 Просмотреть файл

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


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


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


+ 8
- 2
.github/workflows/e2e.yml Просмотреть файл

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


- name: E2E tests - name: E2E tests
run: | run: |
export ENVIRONMENT="CI" export ENVIRONMENT="CI"
bash .scripts/tests-e2e.sh 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 Просмотреть файл

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


on: [push, pull_request] on: [push, pull_request]


@@ -16,13 +16,6 @@ jobs:
run: | run: |
pip install --upgrade pip pipenv pip install --upgrade pip pipenv
pipenv sync -d pipenv sync -d
- name: Unit tests
run: |
pipenv run python -m 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 - name: Check i18n
run: | run: |
pipenv run python setup.py extract_messages --output-file=/tmp/kosmorro-messages.pot > /dev/null pipenv run python setup.py extract_messages --output-file=/tmp/kosmorro-messages.pot > /dev/null

+ 21
- 0
.github/workflows/pylint.yml Просмотреть файл

@@ -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 Просмотреть файл

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

python .scripts/build/getlangs.py
python setup.py compile_catalog sdist bdist_wheel
twine upload dist/* twine upload dist/*

+ 22
- 0
.github/workflows/unit-tests.yml Просмотреть файл

@@ -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 Просмотреть файл

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

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


/manpage/* /manpage/*
!/manpage/*.md !/manpage/*.md


+ 6
- 1
.scripts/tests-e2e.sh Просмотреть файл

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


# Create the package and install it # 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 "$PIP_BIN install dist/kosmorro-$VERSION.tar.gz" "CI"


assertSuccess kosmorro assertSuccess kosmorro
@@ -81,6 +81,10 @@ 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"
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"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27" 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" 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 # 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"
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o /tmp/document.pdf --no-graph"


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


+ 14
- 3
Makefile Просмотреть файл

@@ -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: env:
@if [[ "$$RELEASE_NUMBER" == "" ]]; \ @if [[ "$$RELEASE_NUMBER" == "" ]]; \
then echo "Missing environment variable: 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 add CHANGELOG.md kosmorrolib/version.py kosmorrolib/locales/messages.pot
git commit -m "build: bump version $$RELEASE_NUMBER" git commit -m "build: bump version $$RELEASE_NUMBER"
git tag "v$$RELEASE_NUMBER" git tag "v$$RELEASE_NUMBER"
git checkout features git checkout features
git merge master git merge master
git checkout master git checkout master


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


+ 1
- 0
Pipfile Просмотреть файл

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


[requires] [requires]
python_version = "3" python_version = "3"

+ 9
- 1
Pipfile.lock Просмотреть файл

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


+ 5
- 1
README.md Просмотреть файл

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


- **A LaTeX distribution:** - **A LaTeX distribution:**
- Linux: install TeXLive through your packages manager. Kosmorro just needs the minimal installation, you don't need any extension. - 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:** - **The `latex` Python library:**
- Arch Linux: the library is available [on the AUR](https://aur.archlinux.org/packages/python-latex) - 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` - 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. 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.



+ 115
- 0
kosmorrolib/assets/pdf/kosmorro.sty Просмотреть файл

@@ -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 Просмотреть файл

@@ -5,12 +5,31 @@
\usepackage[margin=25mm]{geometry} \usepackage[margin=25mm]{geometry}
\usepackage{graphicx} \usepackage{graphicx}
\usepackage{hyperref} \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 % Fix Unicode issues
\DeclareUnicodeCharacter{202F}{~} \DeclareUnicodeCharacter{202F}{~}
\DeclareUnicodeCharacter{00B0}{$^\circ$} \DeclareUnicodeCharacter{00B0}{$^\circ$}


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


\begin{document} \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 \maketitle


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


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




+ 31
- 0
kosmorrolib/core.py Просмотреть файл

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


from datetime import date
from dateutil.relativedelta import relativedelta

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


from kosmorrolib.i18n import _

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


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


return new_list 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 Просмотреть файл

@@ -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]): def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[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()),
@@ -60,6 +66,11 @@ class MoonPhase:
def get_phase(self): def get_phase(self):
return MOON_PHASES[self.identifier] 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): def get_next_phase(self):
if self.identifier == 'NEW_MOON' or self.identifier == 'WAXING_CRESCENT': if self.identifier == 'NEW_MOON' or self.identifier == 'WAXING_CRESCENT':
next_identifier = 'FIRST_QUARTER' next_identifier = 'FIRST_QUARTER'
@@ -69,39 +80,20 @@ class MoonPhase:
next_identifier = 'LAST_QUARTER' next_identifier = 'LAST_QUARTER'
else: else:
next_identifier = 'NEW_MOON' next_identifier = 'NEW_MOON'
return next_identifier


return MOON_PHASES[next_identifier] def serialize(self) -> dict:

return {

'phase': self.identifier,
class Position: 'time': self.time.isoformat() if self.time is not None else None,
def __init__(self, latitude: float, longitude: float): 'next': {
self.latitude = latitude 'phase': self.get_next_phase(),
self.longitude = longitude 'time': self.next_phase_date.isoformat()
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




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


def get_skyfield_object(self) -> SkfPlanet: def get_skyfield_object(self) -> SkfPlanet:
return get_skf_objects()[self.skyfield_name] 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) 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): class Star(Object):
def get_type(self) -> str: def get_type(self) -> str:
@@ -164,7 +161,7 @@ class Satellite(Object):
return 'satellite' return 'satellite'




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


return tuple(object.name for object in self.objects) 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]: 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) 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) 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'] MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']


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

ASTERS = [Star(_('Sun'), 'SUN', radius=696342), ASTERS = [Star(_('Sun'), 'SUN', radius=696342),
Satellite(_('Moon'), 'MOON', radius=1737.4), Satellite(_('Moon'), 'MOON', radius=1737.4),
Planet(_('Mercury'), 'MERCURY', radius=2439.7), Planet(_('Mercury'), 'MERCURY', radius=2439.7),
@@ -240,3 +268,21 @@ ASTERS = [Star(_('Sun'), 'SUN', radius=696342),
Planet(_('Uranus'), 'URANUS BARYCENTER', radius=25559), Planet(_('Uranus'), 'URANUS BARYCENTER', radius=25559),
Planet(_('Neptune'), 'NEPTUNE BARYCENTER', radius=24764), Planet(_('Neptune'), 'NEPTUNE BARYCENTER', radius=24764),
Planet(_('Pluto'), 'PLUTO BARYCENTER', radius=1185)] 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 Просмотреть файл

@@ -20,10 +20,11 @@ from abc import ABC, abstractmethod
import datetime import datetime
import json import json
import os import os
from pathlib import Path
from tabulate import tabulate from tabulate import tabulate
from numpy import int64 from numpy import int64
from termcolor import colored from termcolor import colored
from .data import Object, AsterEphemerides, MoonPhase, Event from .data import ASTERS, Object, AsterEphemerides, MoonPhase, Event
from .i18n import _ from .i18n import _
from .version import VERSION from .version import VERSION
from .exceptions import UnavailableFeatureError from .exceptions import UnavailableFeatureError
@@ -40,31 +41,35 @@ TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M')




class Dumper(ABC): class Dumper(ABC):
def __init__(self, ephemeris: dict, events: [Event], date: datetime.date = datetime.date.today(), timezone: int = 0, def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None,
with_colors: bool = True): date: datetime.date = datetime.date.today(), timezone: int = 0, with_colors: bool = True,
self.ephemeris = ephemeris show_graph: bool = False):
self.ephemerides = ephemerides
self.moon_phase = moon_phase
self.events = events self.events = events
self.date = date self.date = date
self.timezone = timezone self.timezone = timezone
self.with_colors = with_colors self.with_colors = with_colors
self.show_graph = show_graph


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


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

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


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


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


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


if len(self.ephemeris['details']) > 0: if self.ephemerides is not None:
text.append(self.get_asters(self.ephemeris['details'])) 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: if len(self.events) > 0:
text.append('\n'.join([self.style(_('Expected events:'), 'h2'), text.append('\n'.join([self.style(_('Expected events:'), 'h2'),
@@ -173,28 +178,28 @@ class TextDumper(Dumper):


return styles[tag](text) return styles[tag](text)


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


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


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


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


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


@@ -219,7 +224,7 @@ class TextDumper(Dumper):
def get_moon(self, moon_phase: MoonPhase) -> str: def get_moon(self, moon_phase: MoonPhase) -> str:
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(), 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_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT),
next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_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') 'assets', 'png', 'kosmorro-logo.png')
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.ephemeris['moon_phase'].identifier.lower().replace('_', '-'), '.'.join([self.moon_phase.identifier.lower().replace('_', '-'),
'png'])) 'png']))


document = template document = template


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


if len(self.events) == 0: if len(self.events) == 0:
document = self._remove_section(document, 'events') 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 \ document = document \
.replace('+++KOSMORRO-VERSION+++', VERSION) \ .replace('+++KOSMORRO-VERSION+++', VERSION) \
.replace('+++KOSMORRO-LOGO+++', kosmorro_logo_path) \ .replace('+++KOSMORRO-LOGO+++', kosmorro_logo_path) \
@@ -274,41 +290,84 @@ class _LatexDumper(Dumper):
.replace('+++EPHEMERIDES-CULMINATION-TIME+++', _('Culmination time')) \ .replace('+++EPHEMERIDES-CULMINATION-TIME+++', _('Culmination time')) \
.replace('+++EPHEMERIDES-SET-TIME+++', _('Set time')) \ .replace('+++EPHEMERIDES-SET-TIME+++', _('Set time')) \
.replace('+++EPHEMERIDES+++', self._make_ephemerides()) \ .replace('+++EPHEMERIDES+++', self._make_ephemerides()) \
.replace('+++GRAPH_LABEL_HOURS+++', _('hours')) \
.replace('+++MOON-PHASE-GRAPHICS+++', moon_phase_graphics) \ .replace('+++MOON-PHASE-GRAPHICS+++', moon_phase_graphics) \
.replace('+++CURRENT-MOON-PHASE-TITLE+++', _('Moon phase:')) \ .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('+++SECTION-EVENTS+++', _('Expected events')) \
.replace('+++EVENTS+++', self._make_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 return document


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

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

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

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

latex.append(r'\object{%s}{%s}{%s}{%s}' % (ephemeris.object.name,
latex.append(r'\object{%s}{%s}{%s}{%s}' % (aster.name, aster_rise,
aster_rise, aster_culmination,
aster_culmination, aster_set))
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) return ''.join(latex)


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




class PdfDumper(Dumper): class PdfDumper(Dumper):
def __init__(self, ephemerides, events, date=datetime.datetime.now(), timezone=0, with_colors=True): def _convert_dates_to_timezones(self):
super(PdfDumper, self).__init__(ephemerides, events, date=date, timezone=0, with_colors=with_colors) """This method is disabled in this dumper, because the timezone is already converted
self.timezone = timezone in :class:`_LatexDumper`."""


def to_string(self): def to_string(self):
try: try:
latex_dumper = _LatexDumper(self.ephemeris, self.events, latex_dumper = _LatexDumper(self.ephemerides, self.moon_phase, self.events,
date=self.date, timezone=self.timezone, with_colors=self.with_colors) date=self.date, timezone=self.timezone, with_colors=self.with_colors,
show_graph=self.show_graph)
return self._compile(latex_dumper.to_string()) return self._compile(latex_dumper.to_string())
except RuntimeError: except RuntimeError:
raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not" raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not"
@@ -365,4 +425,6 @@ class PdfDumper(Dumper):
if build_pdf is None: if build_pdf is None:
raise RuntimeError('Python latex module not found') 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 Просмотреть файл

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


import datetime import datetime
from typing import Union


from skyfield import almanac
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 .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 from .core import get_skf_objects, get_timescale, get_iau2000b


RISEN_ANGLE = -0.8333 RISEN_ANGLE = -0.8333




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


def get_sun(self, start_time, end_time) -> dict: def moon_phase_at(time: Time):
times, is_risen = find_discrete(start_time, time._nutation_angles = get_iau2000b(time)
end_time, current_earth = earth.at(time)
almanac.sunrise_sunset(get_skf_objects(), self.position)) _, 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] moon_phase_at.rough_period = 7.0 # one lunar phase per week
sunset = times[1] if not is_risen[1] else times[0]


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 times, phase = find_discrete(time1, time2, moon_phase_at)
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 moon_phase_at(time: Time): return skyfield_to_moon_phase(times, phase, today)
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)


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


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 start_time = get_timescale().utc(date.year, date.month, date.day)
def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59)
skyfield_aster = get_skf_objects()[aster.skyfield_name]


def get_angle(time: Time) -> float: for aster in ASTERS:
return position.get_planet_topos().at(time).observe(skyfield_aster).apparent().altaz()[0].degrees rise_times, arr = find_discrete(start_time, end_time, is_risen(aster))

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)
try: 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 culmination_time = culmination_time[0] if len(culmination_time) > 0 else None
except ValueError: except ValueError:
culmination_time = None culmination_time = None
@@ -109,37 +95,6 @@ class EphemeridesComputer:
if set_time is not None: if set_time is not None:
set_time = set_time.utc_datetime().replace(microsecond=0) if set_time is not None else 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) ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster))
return aster return ephemerides

@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

+ 67
- 55
kosmorrolib/locales/messages.pot Просмотреть файл

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: kosmorro 0.7.0\n" "Project-Id-Version: kosmorro 0.7.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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" "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"
@@ -17,6 +17,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.8.0\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 #: kosmorrolib/data.py:32
msgid "New Moon" msgid "New Moon"
msgstr "" msgstr ""
@@ -69,120 +79,124 @@ msgstr ""
msgid "%s's largest elongation" msgid "%s's largest elongation"
msgstr "" msgstr ""


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


#: kosmorrolib/dumper.py:157 #: kosmorrolib/dumper.py:162
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:203 kosmorrolib/dumper.py:272 #: kosmorrolib/dumper.py:208 kosmorrolib/dumper.py:288
msgid "Object" msgid "Object"
msgstr "" msgstr ""


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


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


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


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


#: kosmorrolib/dumper.py:221 #: kosmorrolib/dumper.py:226
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:259 #: kosmorrolib/dumper.py:275
msgid "A Summary of your Sky" msgid "A Summary of your Sky"
msgstr "" msgstr ""


#: kosmorrolib/dumper.py:263 #: kosmorrolib/dumper.py:279
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:269 #: kosmorrolib/dumper.py:285
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:271 #: kosmorrolib/dumper.py:287
msgid "Ephemerides of the day" msgid "Ephemerides of the day"
msgstr "" msgstr ""


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

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


#: kosmorrolib/dumper.py:355 #: kosmorrolib/dumper.py:415
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"
@@ -190,103 +204,101 @@ msgid ""
"information." "information."
msgstr "" msgstr ""


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


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


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


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


#: kosmorrolib/main.py:104 #: kosmorrolib/main.py:116
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
msgid "Running on Python {python_version}" msgid "Running on Python {python_version}"
msgstr "" msgstr ""


#: kosmorrolib/main.py:129 #: kosmorrolib/main.py:122
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:136 #: kosmorrolib/main.py:129
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:145 #: kosmorrolib/main.py:138
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:147 #: kosmorrolib/main.py:140
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:152 #: kosmorrolib/main.py:145
msgid "Show the program version" msgid "Show the program version"
msgstr "" msgstr ""


#: kosmorrolib/main.py:154 #: kosmorrolib/main.py:147
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:156 #: kosmorrolib/main.py:149
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:158 #: kosmorrolib/main.py:151
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:161 #: kosmorrolib/main.py:154
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:164 #: kosmorrolib/main.py:157
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). Defaults to the current date ({default_date})" "format). Defaults to the current date ({default_date})"
msgstr "" msgstr ""


#: kosmorrolib/main.py:168 #: kosmorrolib/main.py:161
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:171 #: kosmorrolib/main.py:164
msgid "Disable the colors in the console." msgid "Disable the colors in the console."
msgstr "" msgstr ""


#: kosmorrolib/main.py:173 #: kosmorrolib/main.py:166
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:169
msgid ""
"Generate a graph instead of a table to show the rise, culmination set "
"times (PDF only)"
msgstr ""


+ 24
- 28
kosmorrolib/main.py Просмотреть файл

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


from kosmorrolib.version import VERSION from . import dumper
from kosmorrolib import dumper from . import core
from kosmorrolib import core from . import events
from kosmorrolib import events from .data import Position, EARTH
from kosmorrolib.i18n import _
from .ephemerides import EphemeridesComputer, Position
from .exceptions import UnavailableFeatureError from .exceptions import UnavailableFeatureError
from .i18n import _
from . import ephemerides
from .version import VERSION




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


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


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


if args.latitude is not None or args.longitude is not 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: 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' print(_('Save the planet and paper!\n'
'Consider printing you PDF document only if really necessary, and use the other side of the sheet.')) 'Consider printing you PDF document only if really necessary, and use the other side of the sheet.'))
if position is None: if position is None:
@@ -63,8 +66,8 @@ def main():
"coordinate."), 'yellow')) "coordinate."), 'yellow'))


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


events_list = events.search_events(compute_date) events_list = events.search_events(compute_date)


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


selected_dumper = output_formats[args.format](ephemerides, events_list, format_dumper = output_formats[output_format](ephemerides=eph, moon_phase=moon_phase, events=events_list,
date=compute_date, timezone=timezone, date=compute_date, timezone=timezone, with_colors=args.colors,
with_colors=args.colors) show_graph=args.show_graph)
output = selected_dumper.to_string() output = format_dumper.to_string()
except UnavailableFeatureError as error: except UnavailableFeatureError as error:
print(colored(error.msg, 'red')) print(colored(error.msg, 'red'))
return 2 return 2
@@ -90,7 +93,7 @@ def main():
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 selected_dumper.is_file_output_needed(): elif not format_dumper.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'))
@@ -99,21 +102,11 @@ def main():
return 0 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}: def get_dumpers() -> {str: dumper.Dumper}:
return { return {
'text': dumper.TextDumper, 'text': dumper.TextDumper,
'json': dumper.JsonDumper, '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, 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. ' help=_('A file to export the output to. If not given, the standard output is used. '
'This argument is needed for PDF format.')) '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() return parser.parse_args()

+ 34
- 0
manpage/README.md Просмотреть файл

@@ -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 Просмотреть файл

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


`--date=`_DATE_, `-d` _DATE_ `--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_ `--timezone=`_TIMEZONE_, `-t` _TIMEZONE_
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
@@ -37,6 +37,9 @@
`--format=`_FORMAT_, `-f` _FORMAT_ `--format=`_FORMAT_, `-f` _FORMAT_
the format under which the information have to be output; one of the following: text, json, pdf 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 ## ENVIRONMENT VARIABLES


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


+ 63
- 0
manpage/kosmorro.7.md Просмотреть файл

@@ -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 Просмотреть файл

@@ -39,9 +39,10 @@ setup(
scripts=['kosmorro'], scripts=['kosmorro'],
include_package_data=True, include_package_data=True,
data_files=[ 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=[ classifiers=[
'Development Status :: 3 - Alpha', 'Development Status :: 3 - Alpha',
'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: Linux',


+ 1
- 0
test/__init__.py Просмотреть файл

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

+ 7
- 0
test/core.py Просмотреть файл

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


from datetime import date
from dateutil.relativedelta import relativedelta



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


self.assertEqual("{'great_variable': 'value', 'another_variable': 'another value'}", str(env)) 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__': if __name__ == '__main__':
unittest.main() unittest.main()

+ 171
- 67
test/dumper.py Просмотреть файл

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


def test_json_dumper_returns_correct_json(self): def test_json_dumper_returns_correct_json(self):
self.assertEqual('{\n' 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' ' "moon_phase": {\n'
' "next_phase_date": "2019-10-21T00:00:00",\n'
' "phase": "FULL_MOON",\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' ' },\n'
' "events": [\n' ' "events": [\n'
' {\n' ' {\n'
' "event_type": "OPPOSITION",\n'
' "objects": [\n' ' "objects": [\n'
' "Mars"\n' ' {\n'
' "name": "Mars",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n' ' ],\n'
' "start_time": "2019-10-14T23:00:00",\n' ' "event": "OPPOSITION",\n'
' "end_time": null,\n' ' "starts_at": "2019-10-14T23:00:00",\n'
' "ends_at": null,\n'
' "details": null\n' ' "details": null\n'
' },\n' ' },\n'
' {\n' ' {\n'
' "event_type": "MAXIMAL_ELONGATION",\n'
' "objects": [\n' ' "objects": [\n'
' "Venus"\n' ' {\n'
' "name": "Venus",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n' ' ],\n'
' "start_time": "2019-10-14T12:00:00",\n' ' "event": "MAXIMAL_ELONGATION",\n'
' "end_time": null,\n' ' "starts_at": "2019-10-14T12:00:00",\n'
' "ends_at": null,\n'
' "details": "42.0\\u00b0"\n' ' "details": "42.0\\u00b0"\n'
' }\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' ' ]\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' 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' ' "moon_phase": {\n'
' "next_phase_date": "2019-10-21T00:00:00",\n'
' "phase": "FULL_MOON",\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' ' },\n'
' "events": [\n' ' "events": [\n'
' {\n' ' {\n'
' "event_type": "OPPOSITION",\n'
' "objects": [\n' ' "objects": [\n'
' "Mars"\n' ' {\n'
' "name": "Mars",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n' ' ],\n'
' "start_time": "2019-10-14T23:00:00",\n' ' "event": "OPPOSITION",\n'
' "end_time": null,\n' ' "starts_at": "2019-10-14T23:00:00",\n'
' "ends_at": null,\n'
' "details": null\n' ' "details": null\n'
' },\n' ' },\n'
' {\n' ' {\n'
' "event_type": "MAXIMAL_ELONGATION",\n'
' "objects": [\n' ' "objects": [\n'
' "Venus"\n' ' {\n'
' "name": "Venus",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n' ' ],\n'
' "start_time": "2019-10-14T12:00:00",\n' ' "event": "MAXIMAL_ELONGATION",\n'
' "end_time": null,\n' ' "starts_at": "2019-10-14T12:00:00",\n'
' "ends_at": null,\n'
' "details": "42.0\\u00b0"\n' ' "details": "42.0\\u00b0"\n'
' }\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' ' ]\n'
'}', JsonDumper(data, '}', JsonDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(),
self._get_events() self._get_events()).to_string())
).to_string())


def test_text_dumper_without_events(self): def test_text_dumper_without_events(self):
ephemerides = self._get_data() ephemerides = self._get_ephemerides()
self.assertEqual('Monday October 14, 2019\n\n' self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n' 'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n' '-------- ----------- ------------------ ----------\n'
@@ -98,9 +122,9 @@ class DumperTestCase(unittest.TestCase):
'Moon phase: Full Moon\n' 'Moon phase: Full Moon\n'
'Last Quarter on Monday October 21, 2019 at 00:00\n\n' 'Last Quarter on Monday October 21, 2019 at 00:00\n\n'
'Note: All the hours are given in UTC.', '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' self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n' 'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n' '-------- ----------- ------------------ ----------\n'
@@ -108,10 +132,10 @@ class DumperTestCase(unittest.TestCase):
'Moon phase: Full Moon\n' 'Moon phase: Full Moon\n'
'Last Quarter on Monday October 21, 2019 at 00:00\n\n' 'Last Quarter on Monday October 21, 2019 at 00:00\n\n'
'Note: All the hours are given in UTC.', '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): def test_text_dumper_with_events(self):
ephemerides = self._get_data() ephemerides = self._get_ephemerides()
self.assertEqual("Monday October 14, 2019\n\n" self.assertEqual("Monday October 14, 2019\n\n"
"Object Rise time Culmination time Set time\n" "Object Rise time Culmination time Set time\n"
"-------- ----------- ------------------ ----------\n" "-------- ----------- ------------------ ----------\n"
@@ -122,10 +146,9 @@ class DumperTestCase(unittest.TestCase):
"23:00 Mars is in opposition\n" "23:00 Mars is in opposition\n"
"12:00 Venus's largest elongation (42.0°)\n\n" "12:00 Venus's largest elongation (42.0°)\n\n"
"Note: All the hours are given in UTC.", "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): def test_text_dumper_without_ephemerides_and_with_events(self):
ephemerides = self._get_data(False)
self.assertEqual('Monday October 14, 2019\n\n' self.assertEqual('Monday October 14, 2019\n\n'
'Moon phase: Full Moon\n' 'Moon phase: Full Moon\n'
'Last Quarter on Monday October 21, 2019 at 00:00\n\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' '23:00 Mars is in opposition\n'
"12:00 Venus's largest elongation (42.0°)\n\n" "12:00 Venus's largest elongation (42.0°)\n\n"
'Note: All the hours are given in UTC.', '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): 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' self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n' 'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ -------------\n' '-------- ----------- ------------------ -------------\n'
@@ -147,9 +172,11 @@ class DumperTestCase(unittest.TestCase):
'Oct 15, 00:00 Mars is in opposition\n' 'Oct 15, 00:00 Mars is in opposition\n'
"13:00 Venus's largest elongation (42.0°)\n\n" "13:00 Venus's largest elongation (42.0°)\n\n"
'Note: All the hours are given in the UTC+1 timezone.', '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' self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n' 'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n' '-------- ----------- ------------------ ----------\n'
@@ -160,10 +187,13 @@ class DumperTestCase(unittest.TestCase):
'22:00 Mars is in opposition\n' '22:00 Mars is in opposition\n'
"11:00 Venus's largest elongation (42.0°)\n\n" "11:00 Venus's largest elongation (42.0°)\n\n"
'Note: All the hours are given in the UTC-1 timezone.', '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): 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, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon') self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\section{\\sffamily Expected events}') 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\{23:00\}\{Mars is in opposition\}')
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}") 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._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_latex_dumper_without_ephemerides(self): 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, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon') self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\section{\\sffamily Expected events}') 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}') self.assertNotRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')


def test_latex_dumper_without_events(self): 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, 'Monday October 14, 2019')
self.assertRegex(latex, 'Full Moon') self.assertRegex(latex, 'Full Moon')
self.assertRegex(latex, r'\\object\{Mars\}\{-\}\{-\}\{-\}') self.assertRegex(latex, r'\\object\{Mars\}\{-\}\{-\}\{-\}')
@@ -196,17 +229,88 @@ class DumperTestCase(unittest.TestCase):


self.assertNotRegex(latex, r'\\section{\\sffamily Expected events}') 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 @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 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 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 set_time = datetime(2019, 10, 14, 23) if aster_rise_set else None


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


@staticmethod @staticmethod
def _get_events(): def _get_events():


+ 37
- 32
test/ephemerides.py Просмотреть файл

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




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


self.assertRegex(star.ephemerides.rise_time.isoformat(), '^2019-11-18T05:41:') @expect_assertions(self.assertRegex, num=3)
self.assertRegex(star.ephemerides.culmination_time.isoformat(), '^2019-11-18T11:45:') def do_assertions(assert_regex):
self.assertRegex(star.ephemerides.set_time.isoformat(), '^2019-11-18T17:48:') 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 ### ### MOON PHASE TESTS ###
################################################################################################################### ###################################################################################################################


def test_moon_phase_new_moon(self): 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.assertEqual('WANING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time) self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') 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.assertEqual('NEW_MOON', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') 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.assertEqual('WAXING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time) self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T')


def test_moon_phase_first_crescent(self): 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.assertEqual('WAXING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time) self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-04T') 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.assertEqual('FIRST_QUARTER', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') 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.assertEqual('WAXING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time) self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T')


def test_moon_phase_full_moon(self): 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.assertEqual('WAXING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time) self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') 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.assertEqual('FULL_MOON', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') 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.assertEqual('WANING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time) self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T')


def test_moon_phase_last_quarter(self): 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.assertEqual('WANING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time) self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') 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.assertEqual('LAST_QUARTER', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') 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.assertEqual('WANING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time) self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T')


def test_moon_phase_prediction(self): def test_moon_phase_prediction(self):
phase = MoonPhase('NEW_MOON', None, None) 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) 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) 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) 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) 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) 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) 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) 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__': if __name__ == '__main__':


+ 48
- 0
test/testutils.py Просмотреть файл

@@ -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

||||||
x
 
000:0
Загрузка…
Отмена
Сохранить