@@ -7,7 +7,7 @@ | |||
| License | GNU AGPL-v3 | |||
**Checklist:** | |||
- [ ] I have updated the manpage | |||
- [ ] I have updated the manpages | |||
<!-- | |||
Replace this notice by a short README for your feature/bugfix. | |||
@@ -18,11 +18,17 @@ jobs: | |||
run: | | |||
sudo apt install ruby | |||
sudo gem install ronn | |||
pip install -U setuptools pip | |||
cd manpage && ronn kosmorro.1.md && cd .. | |||
pip install -U setuptools pip requests wheel Babel | |||
- name: E2E tests | |||
run: | | |||
export ENVIRONMENT="CI" | |||
bash .scripts/tests-e2e.sh | |||
- name: manpage (section 1) | |||
run: | | |||
man -P $(which cat) kosmorro | |||
- name: manpage (section 7) | |||
run: | | |||
man -P $(which cat) 7 kosmorro |
@@ -1,4 +1,4 @@ | |||
name: Python application | |||
name: Internationalization check | |||
on: [push, pull_request] | |||
@@ -16,13 +16,6 @@ jobs: | |||
run: | | |||
pip install --upgrade pip pipenv | |||
pipenv sync -d | |||
- name: Unit tests | |||
run: | | |||
pipenv run python -m coverage run -m unittest test | |||
pipenv run codecov --token=${{ secrets.CODECOV_TOKEN }} | |||
- name: Lint | |||
run: | | |||
pipenv run pylint kosmorro *.py kosmorrolib/*.py | |||
- name: Check i18n | |||
run: | | |||
pipenv run python setup.py extract_messages --output-file=/tmp/kosmorro-messages.pot > /dev/null |
@@ -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 |
@@ -29,8 +29,5 @@ jobs: | |||
POEDITOR_API_ACCESS: ${{ secrets.POEDITOR_API_ACCESS }} | |||
POEDITOR_PROJECT_ID: 306433 | |||
run: | | |||
cd manpage && ronn kosmorro.1.md && cd .. | |||
python .scripts/build/getlangs.py | |||
python setup.py compile_catalog sdist bdist_wheel | |||
make POEDITOR_API_ACCESS="${POEDITOR_API_ACCESS}" POEDITOR_PROJECT_ID="306433" build | |||
twine upload dist/* |
@@ -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 }} |
@@ -6,8 +6,10 @@ kosmorro.egg-info | |||
coverage.xml | |||
node_modules/ | |||
package-lock.json | |||
/kosmorrolib/assets/pdf/* | |||
!/assets/pdf/*.tex | |||
!/kosmorrolib/assets/pdf/*.tex | |||
!/kosmorrolib/assets/pdf/*.sty | |||
/manpage/* | |||
!/manpage/*.md | |||
@@ -73,7 +73,7 @@ echo "==== RUNNING E2E TESTS ====" | |||
echo | |||
# Create the package and install it | |||
assertSuccess "$PYTHON_BIN setup.py sdist" | |||
assertSuccess "make build" | |||
assertSuccess "$PIP_BIN install dist/kosmorro-$VERSION.tar.gz" "CI" | |||
assertSuccess kosmorro | |||
@@ -81,6 +81,10 @@ assertSuccess "kosmorro -h" | |||
assertSuccess "kosmorro -d 2020-01-27" | |||
assertFailure "kosmorro -d yolo-yo-lo" | |||
assertFailure "kosmorro -d 2020-13-32" | |||
assertSuccess "kosmorro --date='+3y 5m3d'" | |||
assertSuccess "kosmorro --date='-1y3d'" | |||
assertFailure "kosmorro --date='+3d4m" | |||
assertFailure "kosmorro -date='3y'" | |||
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624" | |||
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27" | |||
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --timezone=1" | |||
@@ -100,6 +104,7 @@ assertSuccess "$PIP_BIN install latex" "CI" | |||
# Dependencies installed, should not fail | |||
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o /tmp/document.pdf" | |||
assertSuccess "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o /tmp/document.pdf --no-graph" | |||
# man page | |||
assertSuccess "man --pager=cat kosmorro" | |||
@@ -1,3 +1,14 @@ | |||
build: | |||
ronn --roff manpage/kosmorro.1.md | |||
ronn --roff manpage/kosmorro.7.md | |||
if [ "$$POEDITOR_API_ACCESS" != "" ]; then \ | |||
python3 .scripts/build/getlangs.py; \ | |||
python3 setup.py compile_catalog; \ | |||
fi | |||
python3 setup.py sdist bdist_wheel | |||
env: | |||
@if [[ "$$RELEASE_NUMBER" == "" ]]; \ | |||
then echo "Missing environment variable: RELEASE_NUMBER."; \ | |||
@@ -27,9 +38,9 @@ finish-release: env | |||
git add CHANGELOG.md kosmorrolib/version.py kosmorrolib/locales/messages.pot | |||
git commit -m "build: bump version $$RELEASE_NUMBER" | |||
git tag "v$$RELEASE_NUMBER" | |||
git checkout features | |||
git merge master | |||
git checkout master | |||
git checkout features | |||
git merge master | |||
git checkout master | |||
@echo | |||
@echo -e "\e[1mVersion \e[36m$$RELEASE_NUMBER\e[39m successfully tagged!" | |||
@@ -16,6 +16,7 @@ tabulate = "*" | |||
numpy = ">=1.17.0,<2.0.0" | |||
termcolor = "*" | |||
latex = "*" | |||
python-dateutil = "*" | |||
[requires] | |||
python_version = "3" |
@@ -1,7 +1,7 @@ | |||
{ | |||
"_meta": { | |||
"hash": { | |||
"sha256": "67748949d467fbdca22ccc963430b21be215566f8a49b56ac734b44023ce5ff2" | |||
"sha256": "4bcfaa3140b0b5c03886cf6877b37a1eb9df4e175f43d3ce0c1312496cde8515" | |||
}, | |||
"pipfile-spec": 6, | |||
"requires": { | |||
@@ -89,6 +89,14 @@ | |||
"index": "pypi", | |||
"version": "==1.18.2" | |||
}, | |||
"python-dateutil": { | |||
"hashes": [ | |||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", | |||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" | |||
], | |||
"index": "pypi", | |||
"version": "==2.8.1" | |||
}, | |||
"sgp4": { | |||
"hashes": [ | |||
"sha256:0cf5ad876c59d0a5c9fc072b040158cf2efe3cef2610ce7184e1024b74f994b2", | |||
@@ -67,9 +67,13 @@ Kosmorro can export the computation results to PDF files, but this feature requi | |||
- **A LaTeX distribution:** | |||
- Linux: install TeXLive through your packages manager. Kosmorro just needs the minimal installation, you don't need any extension. | |||
- macOS: install [MacTeX](https://www.tug.org/mactex/) | |||
- macOS: install [MacTeX](https://www.tug.org/mactex/), the basic version will suffice: | |||
- from the official website, choose the _smaller download_ | |||
- with Brew: `brew install basictex` | |||
- **The `latex` Python library:** | |||
- Arch Linux: the library is available [on the AUR](https://aur.archlinux.org/packages/python-latex) | |||
- Any other systems: install it through PyPI: `pip install latex` | |||
These dependencies are not installed by default, because they take a lot of place and are not necessary if you are not interested in generating PDF files. | |||
The first time you ask Kosmorro to create a PDF, it may be a little verbose. You can ignore its blahblah. | |||
@@ -0,0 +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 |
@@ -5,12 +5,31 @@ | |||
\usepackage[margin=25mm]{geometry} | |||
\usepackage{graphicx} | |||
\usepackage{hyperref} | |||
\usepackage{kosmorro} | |||
\newcommand{\currentmoonphasetitle}{+++CURRENT-MOON-PHASE-TITLE+++} | |||
\newcommand{\ephemeridesobjecttitle}{+++EPHEMERIDES-OBJECT+++} | |||
\newcommand{\ephemeridesrisetimetitle}{+++EPHEMERIDES-RISE-TIME+++} | |||
\newcommand{\ephemeridesculminationtimetitle}{+++EPHEMERIDES-CULMINATION-TIME+++} | |||
\newcommand{\ephemeridessettimetitle}{+++EPHEMERIDES-SET-TIME+++} | |||
\newcommand{\hourslabel}{+++GRAPH_LABEL_HOURS+++} | |||
\newcommand{\Pluto}{+++ASTER_PLUTO+++} | |||
\newcommand{\Neptune}{+++ASTER_NEPTUNE+++} | |||
\newcommand{\Uranus}{+++ASTER_URANUS+++} | |||
\newcommand{\Saturn}{+++ASTER_SATURN+++} | |||
\newcommand{\Jupiter}{+++ASTER_JUPITER+++} | |||
\newcommand{\Mars}{+++ASTER_MARS+++} | |||
\newcommand{\Venus}{+++ASTER_VENUS+++} | |||
\newcommand{\Mercury}{+++ASTER_MERCURY+++} | |||
\newcommand{\Moon}{+++ASTER_MOON+++} | |||
\newcommand{\Sun}{+++ASTER_SUN+++} | |||
% Fix Unicode issues | |||
\DeclareUnicodeCharacter{202F}{~} | |||
\DeclareUnicodeCharacter{00B0}{$^\circ$} | |||
\hypersetup{pdfinfo={ | |||
\hypersetup{pdfinfo={% | |||
Title={+++DOCUMENT-TITLE+++}, | |||
Creator={Kosmorro v+++KOSMORRO-VERSION+++} | |||
}} | |||
@@ -23,49 +42,6 @@ | |||
\begin{document} | |||
\newcommand{\object}[4]{ | |||
\hline | |||
\textbf{#1} & {#2} & {#3} & {#4}\\ | |||
} | |||
\newcommand{\moonphase}[2]{ | |||
\begin{center} | |||
\begin{minipage}{2cm} | |||
\includegraphics[width=\linewidth]{#1} | |||
\end{minipage} | |||
\hspace{5mm} | |||
\begin{minipage}{7cm} | |||
\textbf{+++CURRENT-MOON-PHASE-TITLE+++}\\#2 | |||
\end{minipage} | |||
\end{center} | |||
} | |||
\newenvironment{ephemerides}{ | |||
\begin{table}[h] | |||
\centering | |||
\begin{tabular}{lccc} | |||
\textbf{+++EPHEMERIDES-OBJECT+++} & | |||
\textbf{+++EPHEMERIDES-RISE-TIME+++} & | |||
\textbf{+++EPHEMERIDES-CULMINATION-TIME+++} & | |||
\textbf{+++EPHEMERIDES-SET-TIME+++}\\ | |||
\hline | |||
}{ | |||
\end{tabular} | |||
\end{table} | |||
} | |||
\newcommand{\event}[2]{ | |||
\textbf{#1} & {#2}\\ | |||
} | |||
\newenvironment{events}{ | |||
\begin{table}[h] | |||
\begin{tabular}{ll} | |||
}{ | |||
\end{tabular} | |||
\end{table} | |||
} | |||
\maketitle | |||
+++INTRODUCTION+++ | |||
@@ -76,7 +52,7 @@ | |||
\section{\sffamily +++SECTION-EPHEMERIDES+++} | |||
\begin{ephemerides} | |||
+++EPHEMERIDES+++ | |||
+++EPHEMERIDES+++ | |||
\end{ephemerides} | |||
%%% END-EPHEMERIDES-SECTION | |||
@@ -21,10 +21,15 @@ import re | |||
from shutil import rmtree | |||
from pathlib import Path | |||
from datetime import date | |||
from dateutil.relativedelta import relativedelta | |||
from skyfield.api import Loader | |||
from skyfield.timelib import Time | |||
from skyfield.nutationlib import iau2000b | |||
from kosmorrolib.i18n import _ | |||
CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache' | |||
class Environment: | |||
@@ -86,3 +91,29 @@ def flatten_list(the_list: list): | |||
new_list.append(item) | |||
return new_list | |||
def get_date(date_arg: str) -> date: | |||
if re.match(r'^\d{4}-\d{2}-\d{2}$', date_arg): | |||
try: | |||
return date.fromisoformat(date_arg) | |||
except ValueError as error: | |||
raise ValueError(_('The date {date} is not valid: {error}').format(date=date_arg, error=error.args[0])) | |||
elif re.match(r'^([+-])(([0-9]+)y)?[ ]?(([0-9]+)m)?[ ]?(([0-9]+)d)?$', date_arg): | |||
def get_offset(date_arg: str, signifier: str): | |||
if re.search(r'([0-9]+)' + signifier, date_arg): | |||
return abs(int(re.search(r'[+-]?([0-9]+)' + signifier, date_arg).group(0)[:-1])) | |||
return 0 | |||
days = get_offset(date_arg, 'd') | |||
months = get_offset(date_arg, 'm') | |||
years = get_offset(date_arg, 'y') | |||
if date_arg[0] == '+': | |||
return date.today() + relativedelta(days=days, months=months, years=years) | |||
return date.today() - relativedelta(days=days, months=months, years=years) | |||
else: | |||
error_msg = _('The date {date} does not match the required YYYY-MM-DD format or the offset format.') | |||
raise ValueError(error_msg.format(date=date_arg)) |
@@ -47,7 +47,13 @@ EVENTS = { | |||
} | |||
class MoonPhase: | |||
class Serializable(ABC): | |||
@abstractmethod | |||
def serialize(self) -> dict: | |||
pass | |||
class MoonPhase(Serializable): | |||
def __init__(self, identifier: str, time: Union[datetime, None], next_phase_date: Union[datetime, None]): | |||
if identifier not in MOON_PHASES.keys(): | |||
raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()), | |||
@@ -60,6 +66,11 @@ class MoonPhase: | |||
def get_phase(self): | |||
return MOON_PHASES[self.identifier] | |||
def get_next_phase_name(self): | |||
next_identifier = self.get_next_phase() | |||
return MOON_PHASES[next_identifier] | |||
def get_next_phase(self): | |||
if self.identifier == 'NEW_MOON' or self.identifier == 'WAXING_CRESCENT': | |||
next_identifier = 'FIRST_QUARTER' | |||
@@ -69,39 +80,20 @@ class MoonPhase: | |||
next_identifier = 'LAST_QUARTER' | |||
else: | |||
next_identifier = 'NEW_MOON' | |||
return next_identifier | |||
return MOON_PHASES[next_identifier] | |||
class Position: | |||
def __init__(self, latitude: float, longitude: float): | |||
self.latitude = latitude | |||
self.longitude = longitude | |||
self.observation_planet = None | |||
self._topos = None | |||
def get_planet_topos(self) -> Topos: | |||
if self.observation_planet is None: | |||
raise TypeError('Observation planet must be set.') | |||
if self._topos is None: | |||
self._topos = self.observation_planet + Topos(latitude_degrees=self.latitude, | |||
longitude_degrees=self.longitude) | |||
return self._topos | |||
class AsterEphemerides: | |||
def __init__(self, | |||
rise_time: Union[datetime, None], | |||
culmination_time: Union[datetime, None], | |||
set_time: Union[datetime, None]): | |||
self.rise_time = rise_time | |||
self.culmination_time = culmination_time | |||
self.set_time = set_time | |||
def serialize(self) -> dict: | |||
return { | |||
'phase': self.identifier, | |||
'time': self.time.isoformat() if self.time is not None else None, | |||
'next': { | |||
'phase': self.get_next_phase(), | |||
'time': self.next_phase_date.isoformat() | |||
} | |||
} | |||
class Object(ABC): | |||
class Object(Serializable): | |||
""" | |||
An astronomical object. | |||
""" | |||
@@ -109,7 +101,6 @@ class Object(ABC): | |||
def __init__(self, | |||
name: str, | |||
skyfield_name: str, | |||
ephemerides: AsterEphemerides or None = None, | |||
radius: float = None): | |||
""" | |||
Initialize an astronomical object | |||
@@ -122,7 +113,6 @@ class Object(ABC): | |||
self.name = name | |||
self.skyfield_name = skyfield_name | |||
self.radius = radius | |||
self.ephemerides = ephemerides | |||
def get_skyfield_object(self) -> SkfPlanet: | |||
return get_skf_objects()[self.skyfield_name] | |||
@@ -143,6 +133,13 @@ class Object(ABC): | |||
return 360 / pi * arcsin(self.radius / from_place.at(time).observe(self.get_skyfield_object()).distance().km) | |||
def serialize(self) -> dict: | |||
return { | |||
'name': self.name, | |||
'type': self.get_type(), | |||
'radius': self.radius, | |||
} | |||
class Star(Object): | |||
def get_type(self) -> str: | |||
@@ -164,7 +161,7 @@ class Satellite(Object): | |||
return 'satellite' | |||
class Event: | |||
class Event(Serializable): | |||
def __init__(self, event_type: str, objects: [Object], start_time: datetime, | |||
end_time: Union[datetime, None] = None, details: str = None): | |||
if event_type not in EVENTS.keys(): | |||
@@ -190,6 +187,15 @@ class Event: | |||
return tuple(object.name for object in self.objects) | |||
def serialize(self) -> dict: | |||
return { | |||
'objects': [object.serialize() for object in self.objects], | |||
'event': self.event_type, | |||
'starts_at': self.start_time.isoformat(), | |||
'ends_at': self.end_time.isoformat() if self.end_time is not None else None, | |||
'details': self.details | |||
} | |||
def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonPhase, None]: | |||
tomorrow = get_timescale().utc(now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1) | |||
@@ -228,8 +234,30 @@ def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonP | |||
next_phase_time.utc_datetime() if next_phase_time is not None else None) | |||
class AsterEphemerides(Serializable): | |||
def __init__(self, | |||
rise_time: Union[datetime, None], | |||
culmination_time: Union[datetime, None], | |||
set_time: Union[datetime, None], | |||
aster: Object): | |||
self.rise_time = rise_time | |||
self.culmination_time = culmination_time | |||
self.set_time = set_time | |||
self.object = aster | |||
def serialize(self) -> dict: | |||
return { | |||
'object': self.object.serialize(), | |||
'rise_time': self.rise_time.isoformat() if self.rise_time is not None else None, | |||
'culmination_time': self.culmination_time.isoformat() if self.culmination_time is not None else None, | |||
'set_time': self.set_time.isoformat() if self.set_time is not None else None | |||
} | |||
MONTHS = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] | |||
EARTH = Planet('Earth', 'EARTH') | |||
ASTERS = [Star(_('Sun'), 'SUN', radius=696342), | |||
Satellite(_('Moon'), 'MOON', radius=1737.4), | |||
Planet(_('Mercury'), 'MERCURY', radius=2439.7), | |||
@@ -240,3 +268,21 @@ ASTERS = [Star(_('Sun'), 'SUN', radius=696342), | |||
Planet(_('Uranus'), 'URANUS BARYCENTER', radius=25559), | |||
Planet(_('Neptune'), 'NEPTUNE BARYCENTER', radius=24764), | |||
Planet(_('Pluto'), 'PLUTO BARYCENTER', radius=1185)] | |||
class Position: | |||
def __init__(self, latitude: float, longitude: float, aster: Object): | |||
self.latitude = latitude | |||
self.longitude = longitude | |||
self.aster = aster | |||
self._topos = None | |||
def get_planet_topos(self) -> Topos: | |||
if self.aster is None: | |||
raise TypeError('Observation planet must be set.') | |||
if self._topos is None: | |||
self._topos = self.aster.get_skyfield_object() + Topos(latitude_degrees=self.latitude, | |||
longitude_degrees=self.longitude) | |||
return self._topos |
@@ -20,10 +20,11 @@ from abc import ABC, abstractmethod | |||
import datetime | |||
import json | |||
import os | |||
from pathlib import Path | |||
from tabulate import tabulate | |||
from numpy import int64 | |||
from termcolor import colored | |||
from .data import Object, AsterEphemerides, MoonPhase, Event | |||
from .data import ASTERS, Object, AsterEphemerides, MoonPhase, Event | |||
from .i18n import _ | |||
from .version import VERSION | |||
from .exceptions import UnavailableFeatureError | |||
@@ -40,31 +41,35 @@ TIME_FORMAT = _('{hours}:{minutes}').format(hours='%H', minutes='%M') | |||
class Dumper(ABC): | |||
def __init__(self, ephemeris: dict, events: [Event], date: datetime.date = datetime.date.today(), timezone: int = 0, | |||
with_colors: bool = True): | |||
self.ephemeris = ephemeris | |||
def __init__(self, ephemerides: [AsterEphemerides] = None, moon_phase: MoonPhase = None, events: [Event] = None, | |||
date: datetime.date = datetime.date.today(), timezone: int = 0, with_colors: bool = True, | |||
show_graph: bool = False): | |||
self.ephemerides = ephemerides | |||
self.moon_phase = moon_phase | |||
self.events = events | |||
self.date = date | |||
self.timezone = timezone | |||
self.with_colors = with_colors | |||
self.show_graph = show_graph | |||
if self.timezone != 0: | |||
self._convert_dates_to_timezones() | |||
def _convert_dates_to_timezones(self): | |||
if self.ephemeris['moon_phase'].time is not None: | |||
self.ephemeris['moon_phase'].time = self._datetime_to_timezone(self.ephemeris['moon_phase'].time) | |||
if self.ephemeris['moon_phase'].next_phase_date is not None: | |||
self.ephemeris['moon_phase'].next_phase_date = self._datetime_to_timezone( | |||
self.ephemeris['moon_phase'].next_phase_date) | |||
for aster in self.ephemeris['details']: | |||
if aster.ephemerides.rise_time is not None: | |||
aster.ephemerides.rise_time = self._datetime_to_timezone(aster.ephemerides.rise_time) | |||
if aster.ephemerides.culmination_time is not None: | |||
aster.ephemerides.culmination_time = self._datetime_to_timezone(aster.ephemerides.culmination_time) | |||
if aster.ephemerides.set_time is not None: | |||
aster.ephemerides.set_time = self._datetime_to_timezone(aster.ephemerides.set_time) | |||
if self.moon_phase.time is not None: | |||
self.moon_phase.time = self._datetime_to_timezone(self.moon_phase.time) | |||
if self.moon_phase.next_phase_date is not None: | |||
self.moon_phase.next_phase_date = self._datetime_to_timezone( | |||
self.moon_phase.next_phase_date) | |||
if self.ephemerides is not None: | |||
for ephemeris in self.ephemerides: | |||
if ephemeris.rise_time is not None: | |||
ephemeris.rise_time = self._datetime_to_timezone(ephemeris.rise_time) | |||
if ephemeris.culmination_time is not None: | |||
ephemeris.culmination_time = self._datetime_to_timezone(ephemeris.culmination_time) | |||
if ephemeris.set_time is not None: | |||
ephemeris.set_time = self._datetime_to_timezone(ephemeris.set_time) | |||
for event in self.events: | |||
event.start_time = self._datetime_to_timezone(event.start_time) | |||
@@ -99,11 +104,11 @@ class Dumper(ABC): | |||
class JsonDumper(Dumper): | |||
def to_string(self): | |||
self.ephemeris['events'] = self.events | |||
self.ephemeris['ephemerides'] = self.ephemeris.pop('details') | |||
return json.dumps(self.ephemeris, | |||
default=self._json_default, | |||
indent=4) | |||
return json.dumps({ | |||
'ephemerides': [ephemeris.serialize() for ephemeris in self.ephemerides], | |||
'moon_phase': self.moon_phase.serialize(), | |||
'events': [event.serialize() for event in self.events] | |||
}, indent=4) | |||
@staticmethod | |||
def _json_default(obj): | |||
@@ -139,10 +144,10 @@ class TextDumper(Dumper): | |||
def to_string(self): | |||
text = [self.style(self.get_date_as_string(capitalized=True), 'h1')] | |||
if len(self.ephemeris['details']) > 0: | |||
text.append(self.get_asters(self.ephemeris['details'])) | |||
if self.ephemerides is not None: | |||
text.append(self.stringify_ephemerides()) | |||
text.append(self.get_moon(self.ephemeris['moon_phase'])) | |||
text.append(self.get_moon(self.moon_phase)) | |||
if len(self.events) > 0: | |||
text.append('\n'.join([self.style(_('Expected events:'), 'h2'), | |||
@@ -173,28 +178,28 @@ class TextDumper(Dumper): | |||
return styles[tag](text) | |||
def get_asters(self, asters: [Object]) -> str: | |||
def stringify_ephemerides(self) -> str: | |||
data = [] | |||
for aster in asters: | |||
name = self.style(aster.name, 'th') | |||
for ephemeris in self.ephemerides: | |||
name = self.style(ephemeris.object.name, 'th') | |||
if aster.ephemerides.rise_time is not None: | |||
time_fmt = TIME_FORMAT if aster.ephemerides.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
planet_rise = aster.ephemerides.rise_time.strftime(time_fmt) | |||
if ephemeris.rise_time is not None: | |||
time_fmt = TIME_FORMAT if ephemeris.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
planet_rise = ephemeris.rise_time.strftime(time_fmt) | |||
else: | |||
planet_rise = '-' | |||
if aster.ephemerides.culmination_time is not None: | |||
time_fmt = TIME_FORMAT if aster.ephemerides.culmination_time.day == self.date.day \ | |||
if ephemeris.culmination_time is not None: | |||
time_fmt = TIME_FORMAT if ephemeris.culmination_time.day == self.date.day \ | |||
else SHORT_DATETIME_FORMAT | |||
planet_culmination = aster.ephemerides.culmination_time.strftime(time_fmt) | |||
planet_culmination = ephemeris.culmination_time.strftime(time_fmt) | |||
else: | |||
planet_culmination = '-' | |||
if aster.ephemerides.set_time is not None: | |||
time_fmt = TIME_FORMAT if aster.ephemerides.set_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
planet_set = aster.ephemerides.set_time.strftime(time_fmt) | |||
if ephemeris.set_time is not None: | |||
time_fmt = TIME_FORMAT if ephemeris.set_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
planet_set = ephemeris.set_time.strftime(time_fmt) | |||
else: | |||
planet_set = '-' | |||
@@ -219,7 +224,7 @@ class TextDumper(Dumper): | |||
def get_moon(self, moon_phase: MoonPhase) -> str: | |||
current_moon_phase = ' '.join([self.style(_('Moon phase:'), 'strong'), moon_phase.get_phase()]) | |||
new_moon_phase = _('{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}').format( | |||
next_moon_phase=moon_phase.get_next_phase(), | |||
next_moon_phase=moon_phase.get_next_phase_name(), | |||
next_moon_phase_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT), | |||
next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_FORMAT) | |||
) | |||
@@ -242,17 +247,28 @@ class _LatexDumper(Dumper): | |||
'assets', 'png', 'kosmorro-logo.png') | |||
moon_phase_graphics = os.path.join(os.path.abspath(os.path.dirname(__file__)), | |||
'assets', 'moonphases', 'png', | |||
'.'.join([self.ephemeris['moon_phase'].identifier.lower().replace('_', '-'), | |||
'.'.join([self.moon_phase.identifier.lower().replace('_', '-'), | |||
'png'])) | |||
document = template | |||
if len(self.ephemeris['details']) == 0: | |||
if self.ephemerides is None: | |||
document = self._remove_section(document, 'ephemerides') | |||
if len(self.events) == 0: | |||
document = self._remove_section(document, 'events') | |||
document = self.add_strings(document, kosmorro_logo_path, moon_phase_graphics) | |||
if self.show_graph: | |||
# The graphephemerides environment beginning tag must end with a percent symbol to ensure | |||
# that no extra space will interfere with the graph. | |||
document = document.replace(r'\begin{ephemerides}', r'\begin{graphephemerides}%')\ | |||
.replace(r'\end{ephemerides}', r'\end{graphephemerides}') | |||
return document | |||
def add_strings(self, document, kosmorro_logo_path, moon_phase_graphics) -> str: | |||
document = document \ | |||
.replace('+++KOSMORRO-VERSION+++', VERSION) \ | |||
.replace('+++KOSMORRO-LOGO+++', kosmorro_logo_path) \ | |||
@@ -274,41 +290,84 @@ class _LatexDumper(Dumper): | |||
.replace('+++EPHEMERIDES-CULMINATION-TIME+++', _('Culmination time')) \ | |||
.replace('+++EPHEMERIDES-SET-TIME+++', _('Set time')) \ | |||
.replace('+++EPHEMERIDES+++', self._make_ephemerides()) \ | |||
.replace('+++GRAPH_LABEL_HOURS+++', _('hours')) \ | |||
.replace('+++MOON-PHASE-GRAPHICS+++', moon_phase_graphics) \ | |||
.replace('+++CURRENT-MOON-PHASE-TITLE+++', _('Moon phase:')) \ | |||
.replace('+++CURRENT-MOON-PHASE+++', self.ephemeris['moon_phase'].get_phase()) \ | |||
.replace('+++CURRENT-MOON-PHASE+++', self.moon_phase.get_phase()) \ | |||
.replace('+++SECTION-EVENTS+++', _('Expected events')) \ | |||
.replace('+++EVENTS+++', self._make_events()) | |||
for aster in ASTERS: | |||
document = document.replace('+++ASTER_%s+++' % aster.skyfield_name.upper().split(' ')[0], | |||
aster.name) | |||
return document | |||
def _make_ephemerides(self) -> str: | |||
latex = [] | |||
for aster in self.ephemeris['details']: | |||
if aster.ephemerides.rise_time is not None: | |||
time_fmt = TIME_FORMAT if aster.ephemerides.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
aster_rise = aster.ephemerides.rise_time.strftime(time_fmt) | |||
else: | |||
aster_rise = '-' | |||
if aster.ephemerides.culmination_time is not None: | |||
time_fmt = TIME_FORMAT if aster.ephemerides.culmination_time.day == self.date.day\ | |||
else SHORT_DATETIME_FORMAT | |||
aster_culmination = aster.ephemerides.culmination_time.strftime(time_fmt) | |||
else: | |||
aster_culmination = '-' | |||
if aster.ephemerides.set_time is not None: | |||
time_fmt = TIME_FORMAT if aster.ephemerides.set_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
aster_set = aster.ephemerides.set_time.strftime(time_fmt) | |||
else: | |||
aster_set = '-' | |||
latex.append(r'\object{%s}{%s}{%s}{%s}' % (aster.name, | |||
aster_rise, | |||
aster_culmination, | |||
aster_set)) | |||
graph_y_component = 18 | |||
if self.ephemerides is not None: | |||
for ephemeris in self.ephemerides: | |||
if ephemeris.rise_time is not None: | |||
time_fmt = TIME_FORMAT if ephemeris.rise_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
aster_rise = ephemeris.rise_time.strftime(time_fmt) | |||
else: | |||
aster_rise = '-' | |||
if ephemeris.culmination_time is not None: | |||
time_fmt = TIME_FORMAT if ephemeris.culmination_time.day == self.date.day\ | |||
else SHORT_DATETIME_FORMAT | |||
aster_culmination = ephemeris.culmination_time.strftime(time_fmt) | |||
else: | |||
aster_culmination = '-' | |||
if ephemeris.set_time is not None: | |||
time_fmt = TIME_FORMAT if ephemeris.set_time.day == self.date.day else SHORT_DATETIME_FORMAT | |||
aster_set = ephemeris.set_time.strftime(time_fmt) | |||
else: | |||
aster_set = '-' | |||
if not self.show_graph: | |||
latex.append(r'\object{%s}{%s}{%s}{%s}' % (ephemeris.object.name, | |||
aster_rise, | |||
aster_culmination, | |||
aster_set)) | |||
else: | |||
if ephemeris.rise_time is not None: | |||
raise_hour = ephemeris.rise_time.hour | |||
raise_minute = ephemeris.rise_time.minute | |||
else: | |||
raise_hour = raise_minute = 0 | |||
aster_rise = '' | |||
if ephemeris.set_time is not None: | |||
set_hour = ephemeris.set_time.hour | |||
set_minute = ephemeris.set_time.minute | |||
else: | |||
set_hour = 24 | |||
set_minute = 0 | |||
aster_set = '' | |||
sets_after_end = set_hour > raise_hour | |||
if not sets_after_end: | |||
latex.append(r'\graphobject{%d}{gray}{0}{0}{%d}{%d}{}{%s}' % (graph_y_component, | |||
set_hour, | |||
set_minute, | |||
aster_set)) | |||
set_hour = 24 | |||
set_minute = 0 | |||
latex.append(r'\graphobject{%d}{gray}{%d}{%d}{%d}{%d}{%s}{%s}' % ( | |||
graph_y_component, | |||
raise_hour, | |||
raise_minute, | |||
set_hour, | |||
set_minute, | |||
aster_rise, | |||
aster_set if sets_after_end else '' | |||
)) | |||
graph_y_component -= 2 | |||
return ''.join(latex) | |||
@@ -342,14 +401,15 @@ class _LatexDumper(Dumper): | |||
class PdfDumper(Dumper): | |||
def __init__(self, ephemerides, events, date=datetime.datetime.now(), timezone=0, with_colors=True): | |||
super(PdfDumper, self).__init__(ephemerides, events, date=date, timezone=0, with_colors=with_colors) | |||
self.timezone = timezone | |||
def _convert_dates_to_timezones(self): | |||
"""This method is disabled in this dumper, because the timezone is already converted | |||
in :class:`_LatexDumper`.""" | |||
def to_string(self): | |||
try: | |||
latex_dumper = _LatexDumper(self.ephemeris, self.events, | |||
date=self.date, timezone=self.timezone, with_colors=self.with_colors) | |||
latex_dumper = _LatexDumper(self.ephemerides, self.moon_phase, self.events, | |||
date=self.date, timezone=self.timezone, with_colors=self.with_colors, | |||
show_graph=self.show_graph) | |||
return self._compile(latex_dumper.to_string()) | |||
except RuntimeError: | |||
raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not" | |||
@@ -365,4 +425,6 @@ class PdfDumper(Dumper): | |||
if build_pdf is None: | |||
raise RuntimeError('Python latex module not found') | |||
return bytes(build_pdf(latex_input)) | |||
package = str(Path(__file__).parent.absolute()) + '/assets/pdf/' | |||
return bytes(build_pdf(latex_input, [package])) |
@@ -17,77 +17,63 @@ | |||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
import datetime | |||
from typing import Union | |||
from skyfield import almanac | |||
from skyfield.searchlib import find_discrete, find_maxima | |||
from skyfield.timelib import Time | |||
from skyfield.constants import tau | |||
from .data import Object, Position, AsterEphemerides, MoonPhase, ASTERS, skyfield_to_moon_phase | |||
from .data import Position, AsterEphemerides, MoonPhase, Object, ASTERS, skyfield_to_moon_phase | |||
from .core import get_skf_objects, get_timescale, get_iau2000b | |||
RISEN_ANGLE = -0.8333 | |||
class EphemeridesComputer: | |||
def __init__(self, position: Union[Position, None]): | |||
if position is not None: | |||
position.observation_planet = get_skf_objects()['earth'] | |||
self.position = position | |||
def get_moon_phase(compute_date: datetime.date) -> MoonPhase: | |||
earth = get_skf_objects()['earth'] | |||
moon = get_skf_objects()['moon'] | |||
sun = get_skf_objects()['sun'] | |||
def get_sun(self, start_time, end_time) -> dict: | |||
times, is_risen = find_discrete(start_time, | |||
end_time, | |||
almanac.sunrise_sunset(get_skf_objects(), self.position)) | |||
def moon_phase_at(time: Time): | |||
time._nutation_angles = get_iau2000b(time) | |||
current_earth = earth.at(time) | |||
_, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date') | |||
_, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date') | |||
return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) | |||
sunrise = times[0] if is_risen[0] else times[1] | |||
sunset = times[1] if not is_risen[1] else times[0] | |||
moon_phase_at.rough_period = 7.0 # one lunar phase per week | |||
return {'rise': sunrise, 'set': sunset} | |||
today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day) | |||
time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10) | |||
time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10) | |||
@staticmethod | |||
def get_moon_phase(compute_date: datetime.date) -> MoonPhase: | |||
earth = get_skf_objects()['earth'] | |||
moon = get_skf_objects()['moon'] | |||
sun = get_skf_objects()['sun'] | |||
times, phase = find_discrete(time1, time2, moon_phase_at) | |||
def moon_phase_at(time: Time): | |||
time._nutation_angles = get_iau2000b(time) | |||
current_earth = earth.at(time) | |||
_, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date') | |||
_, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date') | |||
return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int) | |||
return skyfield_to_moon_phase(times, phase, today) | |||
moon_phase_at.rough_period = 7.0 # one lunar phase per week | |||
today = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day) | |||
time1 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day - 10) | |||
time2 = get_timescale().utc(compute_date.year, compute_date.month, compute_date.day + 10) | |||
def get_ephemerides(date: datetime.date, position: Position) -> [AsterEphemerides]: | |||
ephemerides = [] | |||
times, phase = find_discrete(time1, time2, moon_phase_at) | |||
def get_angle(for_aster: Object): | |||
def fun(time: Time) -> float: | |||
return position.get_planet_topos().at(time).observe(for_aster.get_skyfield_object()).apparent().altaz()[0]\ | |||
.degrees | |||
fun.rough_period = 1.0 | |||
return fun | |||
return skyfield_to_moon_phase(times, phase, today) | |||
def is_risen(for_aster: Object): | |||
def fun(time: Time) -> bool: | |||
return get_angle(for_aster)(time) > RISEN_ANGLE | |||
fun.rough_period = 0.5 | |||
return fun | |||
@staticmethod | |||
def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: | |||
skyfield_aster = get_skf_objects()[aster.skyfield_name] | |||
start_time = get_timescale().utc(date.year, date.month, date.day) | |||
end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59) | |||
def get_angle(time: Time) -> float: | |||
return position.get_planet_topos().at(time).observe(skyfield_aster).apparent().altaz()[0].degrees | |||
def is_risen(time: Time) -> bool: | |||
return get_angle(time) > RISEN_ANGLE | |||
get_angle.rough_period = 1.0 | |||
is_risen.rough_period = 0.5 | |||
start_time = get_timescale().utc(date.year, date.month, date.day) | |||
end_time = get_timescale().utc(date.year, date.month, date.day, 23, 59, 59) | |||
rise_times, arr = find_discrete(start_time, end_time, is_risen) | |||
for aster in ASTERS: | |||
rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) | |||
try: | |||
culmination_time, _ = find_maxima(start_time, end_time, f=get_angle, epsilon=1./3600/24, num=12) | |||
culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12) | |||
culmination_time = culmination_time[0] if len(culmination_time) > 0 else None | |||
except ValueError: | |||
culmination_time = None | |||
@@ -109,37 +95,6 @@ class EphemeridesComputer: | |||
if set_time is not None: | |||
set_time = set_time.utc_datetime().replace(microsecond=0) if set_time is not None else None | |||
aster.ephemerides = AsterEphemerides(rise_time, culmination_time, set_time) | |||
return aster | |||
@staticmethod | |||
def is_leap_year(year: int) -> bool: | |||
return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) | |||
def compute_ephemerides(self, compute_date: datetime.date) -> dict: | |||
return {'moon_phase': self.get_moon_phase(compute_date), | |||
'details': [self.get_asters_ephemerides_for_aster(aster, compute_date, self.position) | |||
for aster in ASTERS] if self.position is not None else []} | |||
@staticmethod | |||
def get_seasons(year: int) -> dict: | |||
start_time = get_timescale().utc(year, 1, 1) | |||
end_time = get_timescale().utc(year, 12, 31) | |||
times, almanac_seasons = find_discrete(start_time, end_time, almanac.seasons(get_skf_objects())) | |||
seasons = {} | |||
for time, almanac_season in zip(times, almanac_seasons): | |||
if almanac_season == 0: | |||
season = 'MARCH' | |||
elif almanac_season == 1: | |||
season = 'JUNE' | |||
elif almanac_season == 2: | |||
season = 'SEPTEMBER' | |||
elif almanac_season == 3: | |||
season = 'DECEMBER' | |||
else: | |||
raise AssertionError | |||
seasons[season] = time.utc_iso() | |||
return seasons | |||
ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster)) | |||
return ephemerides |
@@ -8,7 +8,7 @@ msgid "" | |||
msgstr "" | |||
"Project-Id-Version: kosmorro 0.7.0\n" | |||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | |||
"POT-Creation-Date: 2020-04-05 10:47+0200\n" | |||
"POT-Creation-Date: 2020-05-13 13:11+0200\n" | |||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |||
"Language-Team: LANGUAGE <LL@li.org>\n" | |||
@@ -17,6 +17,16 @@ msgstr "" | |||
"Content-Transfer-Encoding: 8bit\n" | |||
"Generated-By: Babel 2.8.0\n" | |||
#: kosmorrolib/core.py:101 | |||
msgid "The date {date} is not valid: {error}" | |||
msgstr "" | |||
#: kosmorrolib/core.py:118 | |||
msgid "" | |||
"The date {date} does not match the required YYYY-MM-DD format or the " | |||
"offset format." | |||
msgstr "" | |||
#: kosmorrolib/data.py:32 | |||
msgid "New Moon" | |||
msgstr "" | |||
@@ -69,120 +79,124 @@ msgstr "" | |||
msgid "%s's largest elongation" | |||
msgstr "" | |||
#: kosmorrolib/data.py:233 | |||
#: kosmorrolib/data.py:261 | |||
msgid "Sun" | |||
msgstr "" | |||
#: kosmorrolib/data.py:234 | |||
#: kosmorrolib/data.py:262 | |||
msgid "Moon" | |||
msgstr "" | |||
#: kosmorrolib/data.py:235 | |||
#: kosmorrolib/data.py:263 | |||
msgid "Mercury" | |||
msgstr "" | |||
#: kosmorrolib/data.py:236 | |||
#: kosmorrolib/data.py:264 | |||
msgid "Venus" | |||
msgstr "" | |||
#: kosmorrolib/data.py:237 | |||
#: kosmorrolib/data.py:265 | |||
msgid "Mars" | |||
msgstr "" | |||
#: kosmorrolib/data.py:238 | |||
#: kosmorrolib/data.py:266 | |||
msgid "Jupiter" | |||
msgstr "" | |||
#: kosmorrolib/data.py:239 | |||
#: kosmorrolib/data.py:267 | |||
msgid "Saturn" | |||
msgstr "" | |||
#: kosmorrolib/data.py:240 | |||
#: kosmorrolib/data.py:268 | |||
msgid "Uranus" | |||
msgstr "" | |||
#: kosmorrolib/data.py:241 | |||
#: kosmorrolib/data.py:269 | |||
msgid "Neptune" | |||
msgstr "" | |||
#: kosmorrolib/data.py:242 | |||
#: kosmorrolib/data.py:270 | |||
msgid "Pluto" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:35 | |||
#: kosmorrolib/dumper.py:36 | |||
msgid "{day_of_week} {month} {day_number}, {year}" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:37 | |||
#: kosmorrolib/dumper.py:38 | |||
msgid "{month} {day_number}, {hours}:{minutes}" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:39 | |||
#: kosmorrolib/dumper.py:40 | |||
msgid "{hours}:{minutes}" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:148 | |||
#: kosmorrolib/dumper.py:153 | |||
msgid "Expected events:" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:152 | |||
#: kosmorrolib/dumper.py:157 | |||
msgid "Note: All the hours are given in UTC." | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:157 | |||
#: kosmorrolib/dumper.py:162 | |||
msgid "Note: All the hours are given in the UTC{offset} timezone." | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:203 kosmorrolib/dumper.py:272 | |||
#: kosmorrolib/dumper.py:208 kosmorrolib/dumper.py:288 | |||
msgid "Object" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:204 kosmorrolib/dumper.py:273 | |||
#: kosmorrolib/dumper.py:209 kosmorrolib/dumper.py:289 | |||
msgid "Rise time" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:205 kosmorrolib/dumper.py:274 | |||
#: kosmorrolib/dumper.py:210 kosmorrolib/dumper.py:290 | |||
msgid "Culmination time" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:206 kosmorrolib/dumper.py:275 | |||
#: kosmorrolib/dumper.py:211 kosmorrolib/dumper.py:291 | |||
msgid "Set time" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:220 kosmorrolib/dumper.py:278 | |||
#: kosmorrolib/dumper.py:225 kosmorrolib/dumper.py:295 | |||
msgid "Moon phase:" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:221 | |||
#: kosmorrolib/dumper.py:226 | |||
msgid "{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:259 | |||
#: kosmorrolib/dumper.py:275 | |||
msgid "A Summary of your Sky" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:263 | |||
#: kosmorrolib/dumper.py:279 | |||
msgid "" | |||
"This document summarizes the ephemerides and the events of {date}. It " | |||
"aims to help you to prepare your observation session. All the hours are " | |||
"given in {timezone}." | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:269 | |||
#: kosmorrolib/dumper.py:285 | |||
msgid "" | |||
"Don't forget to check the weather forecast before you go out with your " | |||
"equipment." | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:271 | |||
#: kosmorrolib/dumper.py:287 | |||
msgid "Ephemerides of the day" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:280 | |||
#: kosmorrolib/dumper.py:293 | |||
msgid "hours" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:297 | |||
msgid "Expected events" | |||
msgstr "" | |||
#: kosmorrolib/dumper.py:355 | |||
#: kosmorrolib/dumper.py:415 | |||
msgid "" | |||
"Building PDFs was not possible, because some dependencies are not " | |||
"installed.\n" | |||
@@ -190,103 +204,101 @@ msgid "" | |||
"information." | |||
msgstr "" | |||
#: kosmorrolib/main.py:58 | |||
#: kosmorrolib/main.py:61 | |||
msgid "" | |||
"Save the planet and paper!\n" | |||
"Consider printing you PDF document only if really necessary, and use the " | |||
"other side of the sheet." | |||
msgstr "" | |||
#: kosmorrolib/main.py:62 | |||
#: kosmorrolib/main.py:65 | |||
msgid "" | |||
"PDF output will not contain the ephemerides, because you didn't provide " | |||
"the observation coordinate." | |||
msgstr "" | |||
#: kosmorrolib/main.py:91 | |||
#: kosmorrolib/main.py:94 | |||
msgid "Could not save the output in \"{path}\": {error}" | |||
msgstr "" | |||
#: kosmorrolib/main.py:96 | |||
#: kosmorrolib/main.py:99 | |||
msgid "Selected output format needs an output file (--output)." | |||
msgstr "" | |||
#: kosmorrolib/main.py:104 | |||
msgid "The date {date} does not match the required YYYY-MM-DD format." | |||
msgstr "" | |||
#: kosmorrolib/main.py:109 | |||
msgid "The date {date} is not valid: {error}" | |||
msgstr "" | |||
#: kosmorrolib/main.py:123 | |||
#: kosmorrolib/main.py:116 | |||
msgid "Running on Python {python_version}" | |||
msgstr "" | |||
#: kosmorrolib/main.py:129 | |||
#: kosmorrolib/main.py:122 | |||
msgid "Do you really want to clear Kosmorro's cache? [yN] " | |||
msgstr "" | |||
#: kosmorrolib/main.py:136 | |||
#: kosmorrolib/main.py:129 | |||
msgid "Answer did not match expected options, cache not cleared." | |||
msgstr "" | |||
#: kosmorrolib/main.py:145 | |||
#: kosmorrolib/main.py:138 | |||
msgid "" | |||
"Compute the ephemerides and the events for a given date, at a given " | |||
"position on Earth." | |||
msgstr "" | |||
#: kosmorrolib/main.py:147 | |||
#: kosmorrolib/main.py:140 | |||
msgid "" | |||
"By default, only the events will be computed for today ({date}).\n" | |||
"To compute also the ephemerides, latitude and longitude arguments are " | |||
"needed." | |||
msgstr "" | |||
#: kosmorrolib/main.py:152 | |||
#: kosmorrolib/main.py:145 | |||
msgid "Show the program version" | |||
msgstr "" | |||
#: kosmorrolib/main.py:154 | |||
#: kosmorrolib/main.py:147 | |||
msgid "Delete all the files Kosmorro stored in the cache." | |||
msgstr "" | |||
#: kosmorrolib/main.py:156 | |||
#: kosmorrolib/main.py:149 | |||
msgid "The format under which the information have to be output" | |||
msgstr "" | |||
#: kosmorrolib/main.py:158 | |||
#: kosmorrolib/main.py:151 | |||
msgid "" | |||
"The observer's latitude on Earth. Can also be set in the " | |||
"KOSMORRO_LATITUDE environment variable." | |||
msgstr "" | |||
#: kosmorrolib/main.py:161 | |||
#: kosmorrolib/main.py:154 | |||
msgid "" | |||
"The observer's longitude on Earth. Can also be set in the " | |||
"KOSMORRO_LONGITUDE environment variable." | |||
msgstr "" | |||
#: kosmorrolib/main.py:164 | |||
#: kosmorrolib/main.py:157 | |||
msgid "" | |||
"The date for which the ephemerides must be computed (in the YYYY-MM-DD " | |||
"format). Defaults to the current date ({default_date})" | |||
msgstr "" | |||
#: kosmorrolib/main.py:168 | |||
#: kosmorrolib/main.py:161 | |||
msgid "" | |||
"The timezone to display the hours in (e.g. 2 for UTC+2 or -3 for UTC-3). " | |||
"Can also be set in the KOSMORRO_TIMEZONE environment variable." | |||
msgstr "" | |||
#: kosmorrolib/main.py:171 | |||
#: kosmorrolib/main.py:164 | |||
msgid "Disable the colors in the console." | |||
msgstr "" | |||
#: kosmorrolib/main.py:173 | |||
#: kosmorrolib/main.py:166 | |||
msgid "" | |||
"A file to export the output to. If not given, the standard output is " | |||
"used. This argument is needed for PDF format." | |||
msgstr "" | |||
#: kosmorrolib/main.py:169 | |||
msgid "" | |||
"Generate a graph instead of a table to show the rise, culmination set " | |||
"times (PDF only)" | |||
msgstr "" | |||
@@ -24,25 +24,28 @@ import sys | |||
from datetime import date | |||
from termcolor import colored | |||
from kosmorrolib.version import VERSION | |||
from kosmorrolib import dumper | |||
from kosmorrolib import core | |||
from kosmorrolib import events | |||
from kosmorrolib.i18n import _ | |||
from .ephemerides import EphemeridesComputer, Position | |||
from . import dumper | |||
from . import core | |||
from . import events | |||
from .data import Position, EARTH | |||
from .exceptions import UnavailableFeatureError | |||
from .i18n import _ | |||
from . import ephemerides | |||
from .version import VERSION | |||
def main(): | |||
environment = core.get_env() | |||
output_formats = get_dumpers() | |||
args = get_args(list(output_formats.keys())) | |||
output_format = args.format | |||
if args.special_action is not None: | |||
return 0 if args.special_action() else 1 | |||
try: | |||
compute_date = get_date(args.date) | |||
compute_date = core.get_date(args.date) | |||
except ValueError as error: | |||
print(colored(error.args[0], color='red', attrs=['bold'])) | |||
return -1 | |||
@@ -50,11 +53,11 @@ def main(): | |||
position = None | |||
if args.latitude is not None or args.longitude is not None: | |||
position = Position(args.latitude, args.longitude) | |||
position = Position(args.latitude, args.longitude, EARTH) | |||
elif environment.latitude is not None and environment.longitude is not None: | |||
position = Position(float(environment.latitude), float(environment.longitude)) | |||
position = Position(float(environment.latitude), float(environment.longitude), EARTH) | |||
if args.format == 'pdf': | |||
if output_format == 'pdf': | |||
print(_('Save the planet and paper!\n' | |||
'Consider printing you PDF document only if really necessary, and use the other side of the sheet.')) | |||
if position is None: | |||
@@ -63,8 +66,8 @@ def main(): | |||
"coordinate."), 'yellow')) | |||
try: | |||
ephemeris = EphemeridesComputer(position) | |||
ephemerides = ephemeris.compute_ephemerides(compute_date) | |||
eph = ephemerides.get_ephemerides(date=compute_date, position=position) if position is not None else None | |||
moon_phase = ephemerides.get_moon_phase(compute_date) | |||
events_list = events.search_events(compute_date) | |||
@@ -75,10 +78,10 @@ def main(): | |||
elif timezone is None: | |||
timezone = 0 | |||
selected_dumper = output_formats[args.format](ephemerides, events_list, | |||
date=compute_date, timezone=timezone, | |||
with_colors=args.colors) | |||
output = selected_dumper.to_string() | |||
format_dumper = output_formats[output_format](ephemerides=eph, moon_phase=moon_phase, events=events_list, | |||
date=compute_date, timezone=timezone, with_colors=args.colors, | |||
show_graph=args.show_graph) | |||
output = format_dumper.to_string() | |||
except UnavailableFeatureError as error: | |||
print(colored(error.msg, 'red')) | |||
return 2 | |||
@@ -90,7 +93,7 @@ def main(): | |||
except OSError as error: | |||
print(_('Could not save the output in "{path}": {error}').format(path=args.output, | |||
error=error.strerror)) | |||
elif not selected_dumper.is_file_output_needed(): | |||
elif not format_dumper.is_file_output_needed(): | |||
print(output) | |||
else: | |||
print(colored(_('Selected output format needs an output file (--output).'), color='red')) | |||
@@ -99,21 +102,11 @@ def main(): | |||
return 0 | |||
def get_date(yyyymmdd: str) -> date: | |||
if not re.match(r'^\d{4}-\d{2}-\d{2}$', yyyymmdd): | |||
raise ValueError(_('The date {date} does not match the required YYYY-MM-DD format.').format(date=yyyymmdd)) | |||
try: | |||
return date.fromisoformat(yyyymmdd) | |||
except ValueError as error: | |||
raise ValueError(_('The date {date} is not valid: {error}').format(date=yyyymmdd, error=error.args[0])) | |||
def get_dumpers() -> {str: dumper.Dumper}: | |||
return { | |||
'text': dumper.TextDumper, | |||
'json': dumper.JsonDumper, | |||
'pdf': dumper.PdfDumper | |||
'pdf': dumper.PdfDumper, | |||
} | |||
@@ -172,5 +165,8 @@ def get_args(output_formats: [str]): | |||
parser.add_argument('--output', '-o', type=str, default=None, | |||
help=_('A file to export the output to. If not given, the standard output is used. ' | |||
'This argument is needed for PDF format.')) | |||
parser.add_argument('--no-graph', dest='show_graph', action='store_false', | |||
help=_('Generate a graph instead of a table to show the rise, culmination set times ' | |||
'(PDF only)')) | |||
return parser.parse_args() |
@@ -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. |
@@ -23,7 +23,7 @@ | |||
the observer's longitude on Earth | |||
`--date=`_DATE_, `-d` _DATE_ | |||
The date for which the ephemerides must be computed (in the YYYY-MM-DD format); defaults to the current date | |||
The date for which the ephemerides must be computed, either in the YYYY-MM-DD format or as an interval in the "[+-]YyMmDd" format (with Y, M, and D numbers); defaults to the current date | |||
`--timezone=`_TIMEZONE_, `-t` _TIMEZONE_ | |||
the timezone to display the hours in; e.g. 2 for UTC+2 or -3 for UTC-3 | |||
@@ -37,6 +37,9 @@ | |||
`--format=`_FORMAT_, `-f` _FORMAT_ | |||
the format under which the information have to be output; one of the following: text, json, pdf | |||
`--no-graph` | |||
present the ephemerides in a table instead of a graph; PDF output format only | |||
## ENVIRONMENT VARIABLES | |||
The environment variable listed below may be used instead of the options. | |||
@@ -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. | |||
@@ -39,9 +39,10 @@ setup( | |||
scripts=['kosmorro'], | |||
include_package_data=True, | |||
data_files=[ | |||
('man/man1', ['manpage/kosmorro.1']) | |||
('man/man1', ['manpage/kosmorro.1']), | |||
('man/man7', ['manpage/kosmorro.7']) | |||
], | |||
install_requires=['skyfield>=1.17.0,<2.0.0', 'tabulate', 'numpy>=1.17.0,<2.0.0', 'termcolor'], | |||
install_requires=['skyfield>=1.17.0,<2.0.0', 'tabulate', 'numpy>=1.17.0,<2.0.0', 'termcolor', 'python-dateutil'], | |||
classifiers=[ | |||
'Development Status :: 3 - Alpha', | |||
'Operating System :: POSIX :: Linux', | |||
@@ -3,3 +3,4 @@ from .data import * | |||
from .dumper import * | |||
from .ephemerides import * | |||
from .events import * | |||
from .testutils import * |
@@ -3,6 +3,9 @@ import unittest | |||
import os | |||
import kosmorrolib.core as core | |||
from datetime import date | |||
from dateutil.relativedelta import relativedelta | |||
class CoreTestCase(unittest.TestCase): | |||
def test_flatten_list(self): | |||
@@ -27,6 +30,10 @@ class CoreTestCase(unittest.TestCase): | |||
self.assertEqual("{'great_variable': 'value', 'another_variable': 'another value'}", str(env)) | |||
def test_date_arg_parsing(self): | |||
self.assertEqual(core.get_date("+1y 2m3d"), date.today() + relativedelta(years=1, months=2, days=3)) | |||
self.assertEqual(core.get_date("-1y2d"), date.today() - relativedelta(years=1, days=2)) | |||
self.assertEqual(core.get_date("1111-11-13"), date(1111, 11, 13)) | |||
if __name__ == '__main__': | |||
unittest.main() |
@@ -11,86 +11,110 @@ class DumperTestCase(unittest.TestCase): | |||
def test_json_dumper_returns_correct_json(self): | |||
self.assertEqual('{\n' | |||
' "ephemerides": [\n' | |||
' {\n' | |||
' "object": {\n' | |||
' "name": "Mars",\n' | |||
' "type": "planet",\n' | |||
' "radius": null\n' | |||
' },\n' | |||
' "rise_time": null,\n' | |||
' "culmination_time": null,\n' | |||
' "set_time": null\n' | |||
' }\n' | |||
' ],\n' | |||
' "moon_phase": {\n' | |||
' "next_phase_date": "2019-10-21T00:00:00",\n' | |||
' "phase": "FULL_MOON",\n' | |||
' "date": "2019-10-14T00:00:00"\n' | |||
' "time": "2019-10-14T00:00:00",\n' | |||
' "next": {\n' | |||
' "phase": "LAST_QUARTER",\n' | |||
' "time": "2019-10-21T00:00:00"\n' | |||
' }\n' | |||
' },\n' | |||
' "events": [\n' | |||
' {\n' | |||
' "event_type": "OPPOSITION",\n' | |||
' "objects": [\n' | |||
' "Mars"\n' | |||
' {\n' | |||
' "name": "Mars",\n' | |||
' "type": "planet",\n' | |||
' "radius": null\n' | |||
' }\n' | |||
' ],\n' | |||
' "start_time": "2019-10-14T23:00:00",\n' | |||
' "end_time": null,\n' | |||
' "event": "OPPOSITION",\n' | |||
' "starts_at": "2019-10-14T23:00:00",\n' | |||
' "ends_at": null,\n' | |||
' "details": null\n' | |||
' },\n' | |||
' {\n' | |||
' "event_type": "MAXIMAL_ELONGATION",\n' | |||
' "objects": [\n' | |||
' "Venus"\n' | |||
' {\n' | |||
' "name": "Venus",\n' | |||
' "type": "planet",\n' | |||
' "radius": null\n' | |||
' }\n' | |||
' ],\n' | |||
' "start_time": "2019-10-14T12:00:00",\n' | |||
' "end_time": null,\n' | |||
' "event": "MAXIMAL_ELONGATION",\n' | |||
' "starts_at": "2019-10-14T12:00:00",\n' | |||
' "ends_at": null,\n' | |||
' "details": "42.0\\u00b0"\n' | |||
' }\n' | |||
' ],\n' | |||
' "ephemerides": [\n' | |||
' {\n' | |||
' "object": "Mars",\n' | |||
' "details": {\n' | |||
' "rise_time": null,\n' | |||
' "culmination_time": null,\n' | |||
' "set_time": null\n' | |||
' }\n' | |||
' }\n' | |||
' ]\n' | |||
'}', JsonDumper(self._get_data(), self._get_events()).to_string()) | |||
'}', JsonDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events()).to_string()) | |||
data = self._get_data(aster_rise_set=True) | |||
self.assertEqual('{\n' | |||
' "ephemerides": [\n' | |||
' {\n' | |||
' "object": {\n' | |||
' "name": "Mars",\n' | |||
' "type": "planet",\n' | |||
' "radius": null\n' | |||
' },\n' | |||
' "rise_time": "2019-10-14T08:00:00",\n' | |||
' "culmination_time": "2019-10-14T13:00:00",\n' | |||
' "set_time": "2019-10-14T23:00:00"\n' | |||
' }\n' | |||
' ],\n' | |||
' "moon_phase": {\n' | |||
' "next_phase_date": "2019-10-21T00:00:00",\n' | |||
' "phase": "FULL_MOON",\n' | |||
' "date": "2019-10-14T00:00:00"\n' | |||
' "time": "2019-10-14T00:00:00",\n' | |||
' "next": {\n' | |||
' "phase": "LAST_QUARTER",\n' | |||
' "time": "2019-10-21T00:00:00"\n' | |||
' }\n' | |||
' },\n' | |||
' "events": [\n' | |||
' {\n' | |||
' "event_type": "OPPOSITION",\n' | |||
' "objects": [\n' | |||
' "Mars"\n' | |||
' {\n' | |||
' "name": "Mars",\n' | |||
' "type": "planet",\n' | |||
' "radius": null\n' | |||
' }\n' | |||
' ],\n' | |||
' "start_time": "2019-10-14T23:00:00",\n' | |||
' "end_time": null,\n' | |||
' "event": "OPPOSITION",\n' | |||
' "starts_at": "2019-10-14T23:00:00",\n' | |||
' "ends_at": null,\n' | |||
' "details": null\n' | |||
' },\n' | |||
' {\n' | |||
' "event_type": "MAXIMAL_ELONGATION",\n' | |||
' "objects": [\n' | |||
' "Venus"\n' | |||
' {\n' | |||
' "name": "Venus",\n' | |||
' "type": "planet",\n' | |||
' "radius": null\n' | |||
' }\n' | |||
' ],\n' | |||
' "start_time": "2019-10-14T12:00:00",\n' | |||
' "end_time": null,\n' | |||
' "event": "MAXIMAL_ELONGATION",\n' | |||
' "starts_at": "2019-10-14T12:00:00",\n' | |||
' "ends_at": null,\n' | |||
' "details": "42.0\\u00b0"\n' | |||
' }\n' | |||
' ],\n' | |||
' "ephemerides": [\n' | |||
' {\n' | |||
' "object": "Mars",\n' | |||
' "details": {\n' | |||
' "rise_time": "2019-10-14T08:00:00",\n' | |||
' "culmination_time": "2019-10-14T13:00:00",\n' | |||
' "set_time": "2019-10-14T23:00:00"\n' | |||
' }\n' | |||
' }\n' | |||
' ]\n' | |||
'}', JsonDumper(data, | |||
self._get_events() | |||
).to_string()) | |||
'}', JsonDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), | |||
self._get_events()).to_string()) | |||
def test_text_dumper_without_events(self): | |||
ephemerides = self._get_data() | |||
ephemerides = self._get_ephemerides() | |||
self.assertEqual('Monday October 14, 2019\n\n' | |||
'Object Rise time Culmination time Set time\n' | |||
'-------- ----------- ------------------ ----------\n' | |||
@@ -98,9 +122,9 @@ class DumperTestCase(unittest.TestCase): | |||
'Moon phase: Full Moon\n' | |||
'Last Quarter on Monday October 21, 2019 at 00:00\n\n' | |||
'Note: All the hours are given in UTC.', | |||
TextDumper(ephemerides, [], date=date(2019, 10, 14), with_colors=False).to_string()) | |||
TextDumper(ephemerides, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string()) | |||
ephemerides = self._get_data(aster_rise_set=True) | |||
ephemerides = self._get_ephemerides(aster_rise_set=True) | |||
self.assertEqual('Monday October 14, 2019\n\n' | |||
'Object Rise time Culmination time Set time\n' | |||
'-------- ----------- ------------------ ----------\n' | |||
@@ -108,10 +132,10 @@ class DumperTestCase(unittest.TestCase): | |||
'Moon phase: Full Moon\n' | |||
'Last Quarter on Monday October 21, 2019 at 00:00\n\n' | |||
'Note: All the hours are given in UTC.', | |||
TextDumper(ephemerides, [], date=date(2019, 10, 14), with_colors=False).to_string()) | |||
TextDumper(ephemerides, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string()) | |||
def test_text_dumper_with_events(self): | |||
ephemerides = self._get_data() | |||
ephemerides = self._get_ephemerides() | |||
self.assertEqual("Monday October 14, 2019\n\n" | |||
"Object Rise time Culmination time Set time\n" | |||
"-------- ----------- ------------------ ----------\n" | |||
@@ -122,10 +146,9 @@ class DumperTestCase(unittest.TestCase): | |||
"23:00 Mars is in opposition\n" | |||
"12:00 Venus's largest elongation (42.0°)\n\n" | |||
"Note: All the hours are given in UTC.", | |||
TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) | |||
TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) | |||
def test_text_dumper_without_ephemerides_and_with_events(self): | |||
ephemerides = self._get_data(False) | |||
self.assertEqual('Monday October 14, 2019\n\n' | |||
'Moon phase: Full Moon\n' | |||
'Last Quarter on Monday October 21, 2019 at 00:00\n\n' | |||
@@ -133,10 +156,12 @@ class DumperTestCase(unittest.TestCase): | |||
'23:00 Mars is in opposition\n' | |||
"12:00 Venus's largest elongation (42.0°)\n\n" | |||
'Note: All the hours are given in UTC.', | |||
TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False).to_string()) | |||
TextDumper(None, self._get_moon_phase(), self._get_events(), | |||
date=date(2019, 10, 14), with_colors=False).to_string()) | |||
def test_timezone_is_taken_in_account(self): | |||
ephemerides = self._get_data(aster_rise_set=True) | |||
ephemerides = self._get_ephemerides(aster_rise_set=True) | |||
self.assertEqual('Monday October 14, 2019\n\n' | |||
'Object Rise time Culmination time Set time\n' | |||
'-------- ----------- ------------------ -------------\n' | |||
@@ -147,9 +172,11 @@ class DumperTestCase(unittest.TestCase): | |||
'Oct 15, 00:00 Mars is in opposition\n' | |||
"13:00 Venus's largest elongation (42.0°)\n\n" | |||
'Note: All the hours are given in the UTC+1 timezone.', | |||
TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False, timezone=1).to_string()) | |||
TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), | |||
with_colors=False, timezone=1).to_string()) | |||
ephemerides = self._get_ephemerides(aster_rise_set=True) | |||
ephemerides = self._get_data(aster_rise_set=True) | |||
self.assertEqual('Monday October 14, 2019\n\n' | |||
'Object Rise time Culmination time Set time\n' | |||
'-------- ----------- ------------------ ----------\n' | |||
@@ -160,10 +187,13 @@ class DumperTestCase(unittest.TestCase): | |||
'22:00 Mars is in opposition\n' | |||
"11:00 Venus's largest elongation (42.0°)\n\n" | |||
'Note: All the hours are given in the UTC-1 timezone.', | |||
TextDumper(ephemerides, self._get_events(), date=date(2019, 10, 14), with_colors=False, timezone=-1).to_string()) | |||
TextDumper(ephemerides, self._get_moon_phase(), self._get_events(), date=date(2019, 10, 14), | |||
with_colors=False, timezone=-1).to_string()) | |||
def test_latex_dumper(self): | |||
latex = _LatexDumper(self._get_data(), self._get_events(), date=date(2019, 10, 14)).to_string() | |||
latex = _LatexDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events(), | |||
date=date(2019, 10, 14)).to_string() | |||
self.assertRegex(latex, 'Monday October 14, 2019') | |||
self.assertRegex(latex, 'Full Moon') | |||
self.assertRegex(latex, r'\\section{\\sffamily Expected events}') | |||
@@ -172,12 +202,14 @@ class DumperTestCase(unittest.TestCase): | |||
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}') | |||
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}") | |||
latex = _LatexDumper(self._get_data(aster_rise_set=True), | |||
latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), | |||
self._get_events(), date=date(2019, 10, 14)).to_string() | |||
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') | |||
def test_latex_dumper_without_ephemerides(self): | |||
latex = _LatexDumper(self._get_data(False), self._get_events(), date=date(2019, 10, 14)).to_string() | |||
latex = _LatexDumper(None, self._get_moon_phase(), self._get_events(), | |||
date=date(2019, 10, 14)).to_string() | |||
self.assertRegex(latex, 'Monday October 14, 2019') | |||
self.assertRegex(latex, 'Full Moon') | |||
self.assertRegex(latex, r'\\section{\\sffamily Expected events}') | |||
@@ -188,7 +220,8 @@ class DumperTestCase(unittest.TestCase): | |||
self.assertNotRegex(latex, r'\\section{\\sffamily Ephemerides of the day}') | |||
def test_latex_dumper_without_events(self): | |||
latex = _LatexDumper(self._get_data(), [], date=date(2019, 10, 14)).to_string() | |||
latex = _LatexDumper(self._get_ephemerides(), self._get_moon_phase(), [], date=date(2019, 10, 14)).to_string() | |||
self.assertRegex(latex, 'Monday October 14, 2019') | |||
self.assertRegex(latex, 'Full Moon') | |||
self.assertRegex(latex, r'\\object\{Mars\}\{-\}\{-\}\{-\}') | |||
@@ -196,17 +229,88 @@ class DumperTestCase(unittest.TestCase): | |||
self.assertNotRegex(latex, r'\\section{\\sffamily Expected events}') | |||
def test_latex_dumper_with_graph(self): | |||
latex = _LatexDumper(self._get_ephemerides(True), self._get_moon_phase(), self._get_events(), | |||
date=date(2019, 10, 14), show_graph=True).to_string() | |||
self.assertRegex(latex, 'Monday October 14, 2019') | |||
self.assertRegex(latex, 'Full Moon') | |||
self.assertRegex(latex, r'\\section{\\sffamily Expected events}') | |||
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}') | |||
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{8\}\{0\}\{23\}\{0\}\{08:00\}\{23:00\}') | |||
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}') | |||
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}") | |||
latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), | |||
self._get_events(), date=date(2019, 10, 14)).to_string() | |||
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') | |||
def test_latex_dumper_with_graph_but_without_rise_time(self): | |||
ephemerides = self._get_ephemerides(True) | |||
ephemerides[0].rise_time = None | |||
latex = _LatexDumper(ephemerides, self._get_moon_phase(), self._get_events(), | |||
date=date(2019, 10, 14), show_graph=True).to_string() | |||
self.assertRegex(latex, 'Monday October 14, 2019') | |||
self.assertRegex(latex, 'Full Moon') | |||
self.assertRegex(latex, r'\\section{\\sffamily Expected events}') | |||
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}') | |||
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{0\}\{0\}\{23\}\{0\}\{\}\{23:00\}') | |||
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}') | |||
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}") | |||
latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), | |||
self._get_events(), date=date(2019, 10, 14)).to_string() | |||
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') | |||
def test_latex_dumper_with_graph_but_without_set_time(self): | |||
ephemerides = self._get_ephemerides(True) | |||
ephemerides[0].set_time = None | |||
latex = _LatexDumper(ephemerides, self._get_moon_phase(), self._get_events(), | |||
date=date(2019, 10, 14), show_graph=True).to_string() | |||
self.assertRegex(latex, 'Monday October 14, 2019') | |||
self.assertRegex(latex, 'Full Moon') | |||
self.assertRegex(latex, r'\\section{\\sffamily Expected events}') | |||
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}') | |||
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{8\}\{0\}\{24\}\{0\}\{08:00\}\{\}') | |||
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}') | |||
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}") | |||
latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), | |||
self._get_events(), date=date(2019, 10, 14)).to_string() | |||
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') | |||
def test_latex_dumper_with_graph_but_mars_sets_tomorrow(self): | |||
ephemerides = self._get_ephemerides(True) | |||
ephemerides[0].set_time = datetime(2019, 10, 15, 1) | |||
latex = _LatexDumper(ephemerides, self._get_moon_phase(), self._get_events(), | |||
date=date(2019, 10, 14), show_graph=True).to_string() | |||
self.assertRegex(latex, 'Monday October 14, 2019') | |||
self.assertRegex(latex, 'Full Moon') | |||
self.assertRegex(latex, r'\\section{\\sffamily Expected events}') | |||
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}') | |||
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{8\}\{0\}\{24\}\{0\}\{08:00\}\{\}') | |||
self.assertRegex(latex, r'\\graphobject\{18\}\{gray\}\{0\}\{0\}\{1\}\{0\}\{\}\{Oct 15, 01:00\}') | |||
self.assertRegex(latex, r'\\event\{23:00\}\{Mars is in opposition\}') | |||
self.assertRegex(latex, r"\\event\{12:00\}\{Venus's largest elongation \(42.0°\)\}") | |||
latex = _LatexDumper(self._get_ephemerides(aster_rise_set=True), self._get_moon_phase(), | |||
self._get_events(), date=date(2019, 10, 14)).to_string() | |||
self.assertRegex(latex, r'\\object\{Mars\}\{08:00\}\{13:00\}\{23:00\}') | |||
@staticmethod | |||
def _get_data(has_ephemerides: bool = True, aster_rise_set=False): | |||
def _get_ephemerides(aster_rise_set=False) -> [AsterEphemerides]: | |||
rise_time = datetime(2019, 10, 14, 8) if aster_rise_set else None | |||
culmination_time = datetime(2019, 10, 14, 13) if aster_rise_set else None | |||
set_time = datetime(2019, 10, 14, 23) if aster_rise_set else None | |||
return { | |||
'moon_phase': MoonPhase('FULL_MOON', datetime(2019, 10, 14), datetime(2019, 10, 21)), | |||
'details': [Planet('Mars', 'MARS', | |||
AsterEphemerides(rise_time, culmination_time, set_time))] if has_ephemerides else [] | |||
} | |||
return [AsterEphemerides(rise_time, culmination_time, set_time, Planet('Mars', 'MARS'))] | |||
@staticmethod | |||
def _get_moon_phase(): | |||
return MoonPhase('FULL_MOON', datetime(2019, 10, 14), datetime(2019, 10, 21)) | |||
@staticmethod | |||
def _get_events(): | |||
@@ -1,106 +1,111 @@ | |||
import unittest | |||
from kosmorrolib.ephemerides import EphemeridesComputer | |||
from kosmorrolib.core import get_skf_objects | |||
from kosmorrolib.data import Star, Position, MoonPhase | |||
from .testutils import expect_assertions | |||
from kosmorrolib import ephemerides | |||
from kosmorrolib.data import EARTH, Position, MoonPhase | |||
from datetime import date | |||
class EphemeridesComputerTestCase(unittest.TestCase): | |||
class EphemeridesTestCase(unittest.TestCase): | |||
def test_get_ephemerides_for_aster_returns_correct_hours(self): | |||
position = Position(0, 0) | |||
position.observation_planet = get_skf_objects()['earth'] | |||
star = EphemeridesComputer.get_asters_ephemerides_for_aster(Star('Sun', skyfield_name='sun'), | |||
date=date(2019, 11, 18), | |||
position=position) | |||
position = Position(0, 0, EARTH) | |||
eph = ephemerides.get_ephemerides(date=date(2019, 11, 18), | |||
position=position) | |||
self.assertRegex(star.ephemerides.rise_time.isoformat(), '^2019-11-18T05:41:') | |||
self.assertRegex(star.ephemerides.culmination_time.isoformat(), '^2019-11-18T11:45:') | |||
self.assertRegex(star.ephemerides.set_time.isoformat(), '^2019-11-18T17:48:') | |||
@expect_assertions(self.assertRegex, num=3) | |||
def do_assertions(assert_regex): | |||
for ephemeris in eph: | |||
if ephemeris.object.skyfield_name == 'SUN': | |||
assert_regex(ephemeris.rise_time.isoformat(), '^2019-11-18T05:41:') | |||
assert_regex(ephemeris.culmination_time.isoformat(), '^2019-11-18T11:45:') | |||
assert_regex(ephemeris.set_time.isoformat(), '^2019-11-18T17:48:') | |||
break | |||
do_assertions() | |||
################################################################################################################### | |||
### MOON PHASE TESTS ### | |||
################################################################################################################### | |||
def test_moon_phase_new_moon(self): | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 25)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 25)) | |||
self.assertEqual('WANING_CRESCENT', phase.identifier) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 26)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 26)) | |||
self.assertEqual('NEW_MOON', phase.identifier) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 27)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 27)) | |||
self.assertEqual('WAXING_CRESCENT', phase.identifier) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-12-04T') | |||
def test_moon_phase_first_crescent(self): | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 3)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 3)) | |||
self.assertEqual('WAXING_CRESCENT', phase.identifier) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-04T') | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 4)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 4)) | |||
self.assertEqual('FIRST_QUARTER', phase.identifier) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 5)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 5)) | |||
self.assertEqual('WAXING_GIBBOUS', phase.identifier) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
def test_moon_phase_full_moon(self): | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 11)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 11)) | |||
self.assertEqual('WAXING_GIBBOUS', phase.identifier) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-12T') | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 12)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 12)) | |||
self.assertEqual('FULL_MOON', phase.identifier) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 13)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 13)) | |||
self.assertEqual('WANING_GIBBOUS', phase.identifier) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
def test_moon_phase_last_quarter(self): | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 18)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 18)) | |||
self.assertEqual('WANING_GIBBOUS', phase.identifier) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-19T') | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 19)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 19)) | |||
self.assertEqual('LAST_QUARTER', phase.identifier) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
phase = EphemeridesComputer.get_moon_phase(date(2019, 11, 20)) | |||
phase = ephemerides.get_moon_phase(date(2019, 11, 20)) | |||
self.assertEqual('WANING_CRESCENT', phase.identifier) | |||
self.assertIsNone(phase.time) | |||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T') | |||
def test_moon_phase_prediction(self): | |||
phase = MoonPhase('NEW_MOON', None, None) | |||
self.assertEqual('First Quarter', phase.get_next_phase()) | |||
self.assertEqual('First Quarter', phase.get_next_phase_name()) | |||
phase = MoonPhase('WAXING_CRESCENT', None, None) | |||
self.assertEqual('First Quarter', phase.get_next_phase()) | |||
self.assertEqual('First Quarter', phase.get_next_phase_name()) | |||
phase = MoonPhase('FIRST_QUARTER', None, None) | |||
self.assertEqual('Full Moon', phase.get_next_phase()) | |||
self.assertEqual('Full Moon', phase.get_next_phase_name()) | |||
phase = MoonPhase('WAXING_GIBBOUS', None, None) | |||
self.assertEqual('Full Moon', phase.get_next_phase()) | |||
self.assertEqual('Full Moon', phase.get_next_phase_name()) | |||
phase = MoonPhase('FULL_MOON', None, None) | |||
self.assertEqual('Last Quarter', phase.get_next_phase()) | |||
self.assertEqual('Last Quarter', phase.get_next_phase_name()) | |||
phase = MoonPhase('WANING_GIBBOUS', None, None) | |||
self.assertEqual('Last Quarter', phase.get_next_phase()) | |||
self.assertEqual('Last Quarter', phase.get_next_phase_name()) | |||
phase = MoonPhase('LAST_QUARTER', None, None) | |||
self.assertEqual('New Moon', phase.get_next_phase()) | |||
self.assertEqual('New Moon', phase.get_next_phase_name()) | |||
phase = MoonPhase('WANING_CRESCENT', None, None) | |||
self.assertEqual('New Moon', phase.get_next_phase()) | |||
self.assertEqual('New Moon', phase.get_next_phase_name()) | |||
if __name__ == '__main__': | |||
@@ -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 |