Browse Source

feat: use Kosmorrolib (#162)

tags/v0.10.0
Jérôme Deuchnord 2 years ago
committed by Jérôme Deuchnord
parent
commit
87206d7068
73 changed files with 1438 additions and 2584 deletions
  1. +19
    -0
      .github/workflows/black.yml
  2. +0
    -66
      .github/workflows/codeql-analysis.yml
  3. +1
    -1
      .github/workflows/i18n.yml
  4. +0
    -26
      .github/workflows/pylint.yml
  5. +0
    -42
      .github/workflows/unit-tests.yml
  6. +29
    -21
      .scripts/tests-e2e.sh
  7. +2
    -2
      MANIFEST.in
  8. +11
    -6
      Makefile
  9. +5
    -6
      Pipfile
  10. +140
    -233
      Pipfile.lock
  11. +0
    -0
      _kosmorro/__init__.py
  12. +8
    -1
      _kosmorro/__version__.py
  13. +0
    -0
      _kosmorro/assets/moonphases/png/first-quarter.png
  14. +0
    -0
      _kosmorro/assets/moonphases/png/full-moon.png
  15. +0
    -0
      _kosmorro/assets/moonphases/png/last-quarter.png
  16. +0
    -0
      _kosmorro/assets/moonphases/png/new-moon.png
  17. +0
    -0
      _kosmorro/assets/moonphases/png/unknown.png
  18. +0
    -0
      _kosmorro/assets/moonphases/png/waning-crescent.png
  19. +0
    -0
      _kosmorro/assets/moonphases/png/waning-gibbous.png
  20. +0
    -0
      _kosmorro/assets/moonphases/png/waxing-crescent.png
  21. +0
    -0
      _kosmorro/assets/moonphases/png/waxing-gibbous.png
  22. +0
    -0
      _kosmorro/assets/moonphases/svg/first-quarter.svg
  23. +0
    -0
      _kosmorro/assets/moonphases/svg/full-moon.svg
  24. +0
    -0
      _kosmorro/assets/moonphases/svg/last-quarter.svg
  25. +0
    -0
      _kosmorro/assets/moonphases/svg/new-moon.svg
  26. +0
    -0
      _kosmorro/assets/moonphases/svg/unknown.svg
  27. +0
    -0
      _kosmorro/assets/moonphases/svg/waning-crescent.svg
  28. +0
    -0
      _kosmorro/assets/moonphases/svg/waning-gibbous.svg
  29. +0
    -0
      _kosmorro/assets/moonphases/svg/waxing-crescent.svg
  30. +0
    -0
      _kosmorro/assets/moonphases/svg/waxing-gibbous.svg
  31. +0
    -0
      _kosmorro/assets/pdf/kosmorro.sty
  32. +0
    -0
      _kosmorro/assets/pdf/template.tex
  33. +0
    -0
      _kosmorro/assets/png/kosmorro-icon-white.png
  34. +0
    -0
      _kosmorro/assets/png/kosmorro-icon.png
  35. +0
    -0
      _kosmorro/assets/png/kosmorro-logo-white.png
  36. +0
    -0
      _kosmorro/assets/png/kosmorro-logo.png
  37. +0
    -0
      _kosmorro/assets/svg/kosmorro-icon-white.svg
  38. +0
    -0
      _kosmorro/assets/svg/kosmorro-icon.svg
  39. +0
    -0
      _kosmorro/assets/svg/kosmorro-logo-white.svg
  40. +0
    -0
      _kosmorro/assets/svg/kosmorro-logo.svg
  41. +42
    -0
      _kosmorro/date.py
  42. +15
    -0
      _kosmorro/debug.py
  43. +527
    -0
      _kosmorro/dumper.py
  44. +35
    -22
      _kosmorro/environment.py
  45. +7
    -4
      _kosmorro/exceptions.py
  46. +0
    -0
      _kosmorro/i18n/__init__.py
  47. +56
    -0
      _kosmorro/i18n/strings.py
  48. +12
    -8
      _kosmorro/i18n/utils.py
  49. +0
    -0
      _kosmorro/locales/de/LC_MESSAGES/messages.po
  50. +0
    -0
      _kosmorro/locales/es/LC_MESSAGES/messages.po
  51. +0
    -0
      _kosmorro/locales/fr/LC_MESSAGES/messages.po
  52. +170
    -166
      _kosmorro/locales/messages.pot
  53. +0
    -0
      _kosmorro/locales/nb_NO/LC_MESSAGES/messages.po
  54. +0
    -0
      _kosmorro/locales/nl/LC_MESSAGES/messages.po
  55. +0
    -0
      _kosmorro/locales/ru/LC_MESSAGES/messages.po
  56. +330
    -0
      _kosmorro/main.py
  57. +3
    -3
      kosmorro
  58. +0
    -119
      kosmorrolib/core.py
  59. +0
    -224
      kosmorrolib/data.py
  60. +0
    -28
      kosmorrolib/dateutil.py
  61. +0
    -385
      kosmorrolib/dumper.py
  62. +0
    -163
      kosmorrolib/ephemerides.py
  63. +0
    -199
      kosmorrolib/events.py
  64. +0
    -195
      kosmorrolib/main.py
  65. +26
    -21
      setup.py
  66. +0
    -7
      test/__init__.py
  67. +0
    -39
      test/core.py
  68. +0
    -17
      test/data.py
  69. +0
    -24
      test/dateutil.py
  70. +0
    -301
      test/dumper.py
  71. +0
    -124
      test/ephemerides.py
  72. +0
    -83
      test/events.py
  73. +0
    -48
      test/testutils.py

+ 19
- 0
.github/workflows/black.yml View File

@@ -0,0 +1,19 @@
name: Code style

on:
push:
branches: [main, features]
pull_request:
branches: [main, features]

jobs:
lint:
runs-on: ubuntu-latest

name: Code Style
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.9
- uses: psf/black@20.8b1

+ 0
- 66
.github/workflows/codeql-analysis.yml View File

@@ -1,66 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"

on:
push:
branches: [master, features]
pull_request:
branches: [master, features]
schedule:
- cron: '0 8 * * 0'

jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
language: ['python']

steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2

# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main

# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1

# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl

# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language

#- run: |
# make bootstrap
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

+ 1
- 1
.github/workflows/i18n.yml View File

@@ -23,7 +23,7 @@ jobs:
- name: Check i18n
run: |
pipenv run python setup.py extract_messages --output-file=/tmp/kosmorro-messages.pot > /dev/null
diff=$(diff kosmorrolib/locales/messages.pot /tmp/kosmorro-messages.pot | grep '^>')
diff=$(diff _kosmorro/locales/messages.pot /tmp/kosmorro-messages.pot | grep '^>')
n=$(echo "$diff" | grep -v '> "POT-Creation-Date: ' | wc -l)

if [ "$(echo "$diff" | grep -E '^"Generated-By: Babel' | wc -l)" -eq "1" ]; then


+ 0
- 26
.github/workflows/pylint.yml View File

@@ -1,26 +0,0 @@
name: Python Lint

on:
push:
branches: [master, features]
pull_request:
branches: [master, features]

jobs:
pylint:
runs-on: ubuntu-latest

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

+ 0
- 42
.github/workflows/unit-tests.yml View File

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

on:
push:
branches: [master, features]
pull_request:
branches: [master, features]

jobs:
unit-tests:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-18.04
- ubuntu-20.04
- macos-10.15
- macos-11.0
python-version:
- '3.7'
- '3.8'
- '3.9'

name: Unit tests (Python ${{ matrix.python-version }} on ${{ matrix.os }})
steps:
- uses: actions/checkout@v1
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install --upgrade pip pipenv
pipenv sync -d
- name: Unit tests
env:
COVERALLS_PRO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
make test
pipenv run coveralls --service=github

+ 29
- 21
.scripts/tests-e2e.sh View File

@@ -1,6 +1,6 @@
#!/bin/bash

VERSION=$(grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' kosmorrolib/version.py)
VERSION=$(grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' _kosmorro/__version__.py)
PYTHON_BIN=$(command -v python)
PIP_BIN=$(command -v pip)

@@ -68,6 +68,8 @@ function assertFailure() {
echo -n '.'
}

mkdir -p $HOME/kosmorro/export

echo
echo "==== RUNNING E2E TESTS ===="
echo
@@ -76,36 +78,42 @@ echo
assertSuccess "make build"
assertSuccess "$PIP_BIN install dist/kosmorro-$VERSION.tar.gz" "CI"

assertSuccess kosmorro
assertSuccess "kosmorro -h"
assertSuccess "kosmorro -d 2020-01-27"
assertFailure "kosmorro -d yolo-yo-lo"
assertFailure "kosmorro -d 2020-13-32"
assertFailure "kosmorro --date=1789-05-05"
assertFailure "kosmorro --date=3000-01-01"
assertSuccess "kosmorro --date='+3y 5m3d'"
assertSuccess "kosmorro --date='-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"
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 --format=json"
assertFailure "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf"
KOSMORRO_COMMAND="kosmorro --debug"

assertSuccess "$KOSMORRO_COMMAND"
assertSuccess "$KOSMORRO_COMMAND -h"
assertSuccess "$KOSMORRO_COMMAND -d 2020-01-27"
assertFailure "$KOSMORRO_COMMAND -d yolo-yo-lo"
assertFailure "$KOSMORRO_COMMAND -d 2020-13-32"
assertFailure "$KOSMORRO_COMMAND --date=1789-05-05"
assertFailure "$KOSMORRO_COMMAND --date=3000-01-01"
assertSuccess "$KOSMORRO_COMMAND --date='+3y 5m3d'"
assertSuccess "$KOSMORRO_COMMAND --date='-1y3d'"
assertFailure "$KOSMORRO_COMMAND --date='+3d4m"
assertFailure "$KOSMORRO_COMMAND -date='3y'"
assertSuccess "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624"
assertSuccess "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624 -d 2020-01-27"
assertSuccess "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --timezone=1"
assertSuccess "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --timezone=-1"
assertSuccess "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=json"
assertFailure "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf"

# Environment variables
assertSuccess "LATITUDE=50.5876 LONGITUDE=3.0624 TIMEZONE=1 kosmorro -d 2020-01-27"
assertSuccess "LATITUDE=50.5876 LONGITUDE=3.0624 TIMEZONE=-1 kosmorro -d 2020-01-27"

# Missing dependencies, should fail
assertFailure "kosmorro --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o /tmp/document.pdf"
assertFailure "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o $HOME/kosmorro/export/document.pdf"
assertFailure "ls $HOME/kosmorro/export/document.pdf"

assertSuccess "sudo apt-get install -y texlive texlive-latex-extra" "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"
assertSuccess "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o $HOME/kosmorro/export/document.pdf"
assertSuccess "ls $HOME/kosmorro/export/document.pdf"

assertSuccess "$KOSMORRO_COMMAND --latitude=50.5876 --longitude=3.0624 -d 2020-01-27 --format=pdf -o $HOME/kosmorro/export/document-no-graph.pdf --no-graph"
assertSuccess "ls $HOME/kosmorro/export/document-no-graph.pdf"

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


+ 2
- 2
MANIFEST.in View File

@@ -1,2 +1,2 @@
recursive-include kosmorrolib/locales *
recursive-include kosmorrolib/assets *
recursive-include _kosmorro/locales *
recursive-include _kosmorro/assets *

+ 11
- 6
Makefile View File

@@ -1,5 +1,7 @@
.PHONY: test
black:
pipenv run black kosmorro _kosmorro setup.py

.PHONY: test
test:
export LC_ALL=C.UTF-8; \
export LANG=C.UTF-8; \
@@ -12,7 +14,7 @@ build: manpages
python3 setup.py sdist bdist_wheel

messages:
pipenv run python setup.py extract_messages --output-file=kosmorrolib/locales/messages.pot
pipenv run python setup.py extract_messages --output-file=_kosmorro/locales/messages.pot

manpages:
ronn --roff manpage/kosmorro.1.md
@@ -32,10 +34,10 @@ release: env
@echo -e "\e[1mCreating release with version number \e[36m$$RELEASE_NUMBER\e[0m"
@echo

sed "s/^VERSION =.*/VERSION = '$$RELEASE_NUMBER'/g" kosmorrolib/version.py > version.py
mv version.py kosmorrolib/version.py
sed "s/^VERSION =.*/VERSION = '$$RELEASE_NUMBER'/g" _kosmorro/version.py > version.py
mv version.py _kosmorro/version.py

pipenv run python setup.py extract_messages --output-file=kosmorrolib/locales/messages.pot > /dev/null
pipenv run python setup.py extract_messages --output-file=_kosmorro/locales/messages.pot > /dev/null

conventional-changelog -p angular -i CHANGELOG.md -s
sed "0,/\\[\\]/s/\\[\\]/[v$$RELEASE_NUMBER]/g" CHANGELOG.md > /tmp/CHANGELOG.md
@@ -47,7 +49,7 @@ release: env
@echo -e "Please review the changes, then invoke \e[33mmake finish-release\e[39m."

finish-release: env
git add CHANGELOG.md kosmorrolib/version.py kosmorrolib/locales/messages.pot
git add CHANGELOG.md _kosmorro/version.py _kosmorro/locales/messages.pot
git commit -m "build: bump version $$RELEASE_NUMBER"
git tag "v$$RELEASE_NUMBER"
git checkout features
@@ -57,3 +59,6 @@ finish-release: env
@echo
@echo -e "\e[1mVersion \e[36m$$RELEASE_NUMBER\e[39m successfully tagged!"
@echo -e "Invoke \e[33mgit push origin master features v$$RELEASE_NUMBER\e[39m to finish."

clean:
rm -rf build dist kosmorro.egg-info manpage/kosmorro.{1,7}{,.html}

+ 5
- 6
Pipfile View File

@@ -4,18 +4,17 @@ url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]
pylintfileheader = "*"
pylint = "*"
babel = "*"
coveralls = "*"
parameterized = "*"
black = "*"

[packages]
skyfield = ">=1.32.0,<2.0.0"
tabulate = "*"
numpy = ">=1.17.0,<2.0.0"
termcolor = "*"
kosmorrolib = ">=0.11.0,<0.12.0"
python-dateutil = "*"

[requires]
python_version = "3"

[pipenv]
allow_prereleases = true

+ 140
- 233
Pipfile.lock View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "365dcb1506ef7a3b208eac9fb441ecc5624b47c83a74f02ed28bbdde67f23549"
"sha256": "7ca2ce1c14eec2a7542613e3f502550e9facb550e09cd2430e2d73ac4fe2fa10"
},
"pipfile-spec": 6,
"requires": {
@@ -29,35 +29,43 @@
],
"version": "==2.15"
},
"numpy": {
"kosmorrolib": {
"hashes": [
"sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29",
"sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab",
"sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04",
"sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d",
"sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590",
"sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f",
"sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a",
"sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845",
"sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090",
"sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b",
"sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e",
"sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc",
"sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257",
"sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e",
"sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff",
"sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9",
"sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b",
"sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e",
"sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0",
"sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb",
"sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f",
"sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2",
"sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14",
"sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9"
"sha256:9a3059a8df2ec4ec83005be0cba3e4620cb005e37893a274f27e4739fa251091",
"sha256:ec1d252fc9ec5ab387f0b49d94a5e4dcb4f99cfb8f0661330c1ae23f2f2b1144"
],
"index": "pypi",
"version": "==1.20.1"
"version": "==0.11.2"
},
"numpy": {
"hashes": [
"sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727",
"sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6",
"sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98",
"sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7",
"sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d",
"sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2",
"sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9",
"sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935",
"sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff",
"sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee",
"sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb",
"sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042",
"sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3",
"sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5",
"sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6",
"sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f",
"sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4",
"sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737",
"sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931",
"sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6",
"sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677",
"sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576",
"sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935",
"sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd"
],
"markers": "python_version >= '3.7'",
"version": "==1.20.2"
},
"python-dateutil": {
"hashes": [
@@ -69,50 +77,50 @@
},
"sgp4": {
"hashes": [
"sha256:0bcc2e078d04a32f0db1278ecf434b85d51682ab0d6144490940f8a7479305c9",
"sha256:15745e778634c7aea8a7d53f0108562ee80d3f9f4e2812ab1884b20f5fb1f995",
"sha256:19cd0d8e2c0826819bd8e46ffcf7866d137078b4110603c644a9eb3427347fe6",
"sha256:2cb573d4831db7ebfcd80442ba72cd3cbcae99d33bcc8ac298186bb358515cad",
"sha256:465dc1ef603aac17f59a1b00716c1f0187b90eaf88ce14ff6b19a40c7dd4ad97",
"sha256:6a7b9c109664284df3914e71bc9f773eda574d920763de5aac882e4e02002730",
"sha256:786b8e674d23c9efa634b782dc9650970e8662c92bc7ba457296fad84afaf34a",
"sha256:7b638875f7072926e57145537e831128a9bc6fed54c52fac8f5248fb775acc3e",
"sha256:802db53c0bb3caf58f69e38fbb04d7648c98ec4b34e17b4c3e9f81308352a9e1",
"sha256:850e16475a13cb11ec12c69c9b91d03e8f61461b083d9c6232d6119c2777a13f",
"sha256:85fb39c28741270e1552bc90269e1d73d65eb1383696848100dd991622a87bf7",
"sha256:8777dc09d027b4bd74675c91cf2284d79f800e11a22d0e18acca1611c49ae955",
"sha256:968760092038672e6e0686b581090016d491bb373a41416d2898f334bfa831e8",
"sha256:96b8359c520e0e66d6d9ec0d775c948401887b8b60ee0775d3b6ad465f13ba6a",
"sha256:9a097c1f7f777bd3eb36578cc195253053794396e34e34d67e7e2c7fb6960fda",
"sha256:9e6b0a476a9481aec19a23d69292b0009a465962decc15d22407a99aa4ad306a",
"sha256:a427bb73931d91dcc5e8b8ca796636b862f172c471ebc000e43eed6d84e2ee72",
"sha256:ae4d2401eff33a31737ec15a947a97bebe784701e064bb1d3096e05b068ca5b8",
"sha256:b110b2e4cb9223913e48e1f1ed5783d52a29e97556fa89dfdef721d23d42b134",
"sha256:bc17cc051246e21b0e63a536d236ddec6e27761107c7c3acfb6cdfe6c61627d8",
"sha256:bc443f34a0a3d6c3b02701403c0cffc9af4a3e864f41168cd0fc6b416e27ff58",
"sha256:c13f98a9227fb5a242c9ea17bcc701e220deb75c576d240581107d4e9d20ce80",
"sha256:c3a17700f19c881cb816bc7998ccb32e07ad5dc1672affff90324e09f3262025",
"sha256:c600da1a26257b733df9de5e8a916f06160b21f819f073cd56cbe7f3be709877",
"sha256:cb07c686b2f5552f8048ad3cace371dac1e1a530fb1ce20031d1ef46854793c1",
"sha256:cff9b03cf770753522face8be7fec52d4ba9eee0fe9d73d324fa0724343a97a5",
"sha256:d31cd0347ddd3bbeee2c47867f0c8a8f9ed0d94815f902aa62aab676c740526b",
"sha256:dafc9bf8d52d13d82490b27b1d93a7189e408e1b7b83a838db4f39b21718053e"
],
"version": "==2.18"
"sha256:0e9d5764a60fa92a1f58fd5d5bc24f44a36e48e34723967951f6ca15330bc984",
"sha256:11a1d84c940462127a651368aced98e25f8cddf3ee6f3fff086c88549e3afb50",
"sha256:12bf193480bb734233b67c32dbead214a292cd451062eeb48cda5fe3a6b1bc44",
"sha256:15aa5c2fc22fa8995e3ddaaae3da65283c8e6d7c3c2c784a0b2ed2cb60ba0efe",
"sha256:16ed70a83dc5249b41d1dc2a5d129216fb8ccbda67ce4ea6d8aea47b1cae46cf",
"sha256:2274a7a2b88905c0de12fcff0ffc51a24a8e2e936d4f02313f9c2816273ebbb9",
"sha256:2a045b230f7b7afa095b2df434f3d84cf23235134a31e183c2a94ab6cf6bf644",
"sha256:2f2f54948b04b6bd472926b7fcb64f07c535a85bd7395006e766528bd47dc077",
"sha256:3755a35cefb6d9884223ebcab8453264f03d9cde5287a72d7109c9e832e13095",
"sha256:50e89196e610022f9ff2766a3cb31aae3bb7c98e79db0bf3352376ff2e2e3ea8",
"sha256:549ceaba69e8426a072d95498c60d81b5803c962ff78e35a7696ab1634d9b55d",
"sha256:5774221bcf1cbb21bee9bbaede05d4696651e1050e050b6ca5fb46a3b7cda83a",
"sha256:584f0b6acd53f523340af98b746c595befbb939426ddc6d604c2ea2b006822a5",
"sha256:9166d85f34e26ae66e828ca6748dbfc28ef45cd2be6478e418ccf24e6cdbd333",
"sha256:972322e6ef456ca15ae857339585612b27843ca3afc25cc8ee93ee97efce1e94",
"sha256:981b6977ec0136eb733ffe0fb852ce34dd1994dbae01f53f7d9b8fba13b18e85",
"sha256:a172ca0d65439ff9ca2824d6e4e12d9587b0226e3511204298b11cdbe280b72a",
"sha256:ae35b79bb3837044e81af6ab6cfa74e2e0293d1a471d60c295fac6747c5b2ac9",
"sha256:b2dcfcc2e41c229669caa101740bd7f37f8539b16a138f9683593e7de0dee3d6",
"sha256:b68ffc1a08bc4b7270e0d34c40efd35010cc49a5c1cf0904097e17591ee4e922",
"sha256:c9b7a85e3ce83908247ca6e5633d37543973562cbed2f076d667874f0c7396d9",
"sha256:cfa86f71cc394ad6b555edaee9cccb16e48a253c99038a63124b86476645f491",
"sha256:d4d95f789785319771782d06aa6fcdef0cf4fb749995b50c82156ea7a9713ed6",
"sha256:de8f77ddc146a0b6bc6ccf7b0355990b3c0928fc1a16a63fd8121d410d717a42",
"sha256:e64f985c99286467a2f84de8cc6e99832af6819eb52ad4e6106a4df5af7bc02d",
"sha256:eab7a12baa13057db6456ce031d98a58ca62daa59b5988612ad70c33309f35c0",
"sha256:f31ed36be5d8950f759b344a52d1ed6926cc1e93061cb2a9c4096ed34c74fa94",
"sha256:ffad58b31bf1316829fa432f6f63fbdff80c1b2d9012926ae4e1f294a8772087"
],
"version": "==2.19"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"version": "==1.15.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"skyfield": {
"hashes": [
"sha256:64d2716187b94ccb587ec6db46ecb252fb14ecc3b32ef37ce6e90683bb5956cb"
"sha256:5a8da9720b49981bc6fc60ab8d52ef42a8d46fe7161e34ef6dd1d496de9903e2"
],
"index": "pypi",
"version": "==1.37"
"version": "==1.39"
},
"tabulate": {
"hashes": [
@@ -131,178 +139,51 @@
}
},
"develop": {
"astroid": {
"appdirs": {
"hashes": [
"sha256:6b0ed1af831570e500e2437625979eaa3b36011f66ddfc4ce930128610258ca9",
"sha256:cd80bf957c49765dce6d92c43163ff9d2abc43132ce64d4b1b47717c6d2522df"
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
],
"version": "==2.5.2"
"version": "==1.5.4"
},
"babel": {
"hashes": [
"sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5",
"sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"
"sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9",
"sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"
],
"index": "pypi",
"version": "==2.9.0"
"version": "==2.9.1"
},
"certifi": {
"black": {
"hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.12.5"
},
"chardet": {
"hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==4.0.0"
},
"coverage": {
"hashes": [
"sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c",
"sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6",
"sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45",
"sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a",
"sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03",
"sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529",
"sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a",
"sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a",
"sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2",
"sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6",
"sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759",
"sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53",
"sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a",
"sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4",
"sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff",
"sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502",
"sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793",
"sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb",
"sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905",
"sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821",
"sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b",
"sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81",
"sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0",
"sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b",
"sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3",
"sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184",
"sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701",
"sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a",
"sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82",
"sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638",
"sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5",
"sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083",
"sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6",
"sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90",
"sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465",
"sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a",
"sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3",
"sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e",
"sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066",
"sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf",
"sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b",
"sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae",
"sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669",
"sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873",
"sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b",
"sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6",
"sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb",
"sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160",
"sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c",
"sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079",
"sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d",
"sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"
],
"version": "==5.5"
},
"coveralls": {
"hashes": [
"sha256:7bd173b3425733661ba3063c88f180127cc2b20e9740686f86d2622b31b41385",
"sha256:cbb942ae5ef3d2b55388cb5b43e93a269544911535f1e750e1c656aef019ce60"
"sha256:0e80435b8a88f383c9149ae89d671eb2095b72344b0fe8a1d61d2ff5110ed173",
"sha256:9dc2042018ca10735366d944c2c12d9cad6dec74a3d5f679d09384ea185d9943"
],
"index": "pypi",
"version": "==3.0.1"
},
"docopt": {
"hashes": [
"sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"
],
"version": "==0.6.2"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"isort": {
"hashes": [
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
],
"version": "==5.8.0"
"version": "==21.5b0"
},
"lazy-object-proxy": {
"click": {
"hashes": [
"sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653",
"sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61",
"sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2",
"sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837",
"sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3",
"sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43",
"sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726",
"sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3",
"sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587",
"sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8",
"sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a",
"sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd",
"sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f",
"sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad",
"sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4",
"sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b",
"sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf",
"sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981",
"sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741",
"sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e",
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
"sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"version": "==1.6.0"
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==7.1.2"
},
"mccabe": {
"mypy-extensions": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.6.1"
"version": "==0.4.3"
},
"parameterized": {
"pathspec": {
"hashes": [
"sha256:41bbff37d6186430f77f900d777e5bb6a24928a1c46fb1de692f8b52b8833b5c",
"sha256:9cbb0b69a03e8695d68b3399a8a5825200976536fe1cb79db60ed6a4c8c9efe9"
"sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd",
"sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"
],
"index": "pypi",
"version": "==0.8.1"
},
"pylint": {
"hashes": [
"sha256:0e21d3b80b96740909d77206d741aa3ce0b06b41be375d92e1f3244a274c1f8a",
"sha256:d09b0b07ba06bcdff463958f53f23df25e740ecd81895f7d2699ec04bbd8dc3b"
],
"index": "pypi",
"version": "==2.7.2"
},
"pylintfileheader": {
"hashes": [
"sha256:7871193691484210268d467dc12d88ac5b3ba7eb7dec6239e24075797185a3b2",
"sha256:a23f143b0fb4d65f984ffd824731d6e41f2840e26a5752a90df93f4454b5ccd1"
],
"index": "pypi",
"version": "==0.3.0"
},
"pytz": {
"hashes": [
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
@@ -310,33 +191,59 @@
],
"version": "==2021.1"
},
"requests": {
"hashes": [
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"version": "==2.25.1"
"regex": {
"hashes": [
"sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5",
"sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79",
"sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31",
"sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500",
"sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11",
"sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14",
"sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3",
"sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439",
"sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c",
"sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82",
"sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711",
"sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093",
"sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a",
"sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb",
"sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8",
"sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17",
"sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000",
"sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d",
"sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480",
"sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc",
"sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0",
"sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9",
"sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765",
"sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e",
"sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a",
"sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07",
"sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f",
"sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac",
"sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7",
"sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed",
"sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968",
"sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7",
"sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2",
"sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4",
"sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87",
"sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8",
"sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10",
"sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29",
"sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605",
"sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6",
"sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"
],
"version": "==2021.4.4"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"urllib3": {
"hashes": [
"sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
"sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
],
"index": "pypi",
"version": "==1.26.4"
},
"wrapt": {
"hashes": [
"sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
],
"version": "==1.12.1"
}
}
}

kosmorrolib/__init__.py → _kosmorro/__init__.py View File


kosmorrolib/version.py → _kosmorro/__version__.py View File

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

VERSION = '0.9.0'
__title__ = "kosmorrolib"
__description__ = "A program that computes your ephemerides"
__url__ = "http://kosmorro.space"
__version__ = "0.9.0"
__author__ = "Jérôme Deuchnord"
__author_email__ = "jerome@deuchnord.fr"
__license__ = "AGPL"
__copyright__ = "Copyright 2021 Jérôme Deuchnord"

kosmorrolib/assets/moonphases/png/first-quarter.png → _kosmorro/assets/moonphases/png/first-quarter.png View File


kosmorrolib/assets/moonphases/png/full-moon.png → _kosmorro/assets/moonphases/png/full-moon.png View File


kosmorrolib/assets/moonphases/png/last-quarter.png → _kosmorro/assets/moonphases/png/last-quarter.png View File


kosmorrolib/assets/moonphases/png/new-moon.png → _kosmorro/assets/moonphases/png/new-moon.png View File


kosmorrolib/assets/moonphases/png/unknown.png → _kosmorro/assets/moonphases/png/unknown.png View File


kosmorrolib/assets/moonphases/png/waning-crescent.png → _kosmorro/assets/moonphases/png/waning-crescent.png View File


kosmorrolib/assets/moonphases/png/waning-gibbous.png → _kosmorro/assets/moonphases/png/waning-gibbous.png View File


kosmorrolib/assets/moonphases/png/waxing-crescent.png → _kosmorro/assets/moonphases/png/waxing-crescent.png View File


kosmorrolib/assets/moonphases/png/waxing-gibbous.png → _kosmorro/assets/moonphases/png/waxing-gibbous.png View File


kosmorrolib/assets/moonphases/svg/first-quarter.svg → _kosmorro/assets/moonphases/svg/first-quarter.svg View File


kosmorrolib/assets/moonphases/svg/full-moon.svg → _kosmorro/assets/moonphases/svg/full-moon.svg View File


kosmorrolib/assets/moonphases/svg/last-quarter.svg → _kosmorro/assets/moonphases/svg/last-quarter.svg View File


kosmorrolib/assets/moonphases/svg/new-moon.svg → _kosmorro/assets/moonphases/svg/new-moon.svg View File


kosmorrolib/assets/moonphases/svg/unknown.svg → _kosmorro/assets/moonphases/svg/unknown.svg View File


kosmorrolib/assets/moonphases/svg/waning-crescent.svg → _kosmorro/assets/moonphases/svg/waning-crescent.svg View File


kosmorrolib/assets/moonphases/svg/waning-gibbous.svg → _kosmorro/assets/moonphases/svg/waning-gibbous.svg View File


kosmorrolib/assets/moonphases/svg/waxing-crescent.svg → _kosmorro/assets/moonphases/svg/waxing-crescent.svg View File


kosmorrolib/assets/moonphases/svg/waxing-gibbous.svg → _kosmorro/assets/moonphases/svg/waxing-gibbous.svg View File


kosmorrolib/assets/pdf/kosmorro.sty → _kosmorro/assets/pdf/kosmorro.sty View File


kosmorrolib/assets/pdf/template.tex → _kosmorro/assets/pdf/template.tex View File


kosmorrolib/assets/png/kosmorro-icon-white.png → _kosmorro/assets/png/kosmorro-icon-white.png View File


kosmorrolib/assets/png/kosmorro-icon.png → _kosmorro/assets/png/kosmorro-icon.png View File


kosmorrolib/assets/png/kosmorro-logo-white.png → _kosmorro/assets/png/kosmorro-logo-white.png View File


kosmorrolib/assets/png/kosmorro-logo.png → _kosmorro/assets/png/kosmorro-logo.png View File


kosmorrolib/assets/svg/kosmorro-icon-white.svg → _kosmorro/assets/svg/kosmorro-icon-white.svg View File


kosmorrolib/assets/svg/kosmorro-icon.svg → _kosmorro/assets/svg/kosmorro-icon.svg View File


kosmorrolib/assets/svg/kosmorro-logo-white.svg → _kosmorro/assets/svg/kosmorro-logo-white.svg View File


kosmorrolib/assets/svg/kosmorro-logo.svg → _kosmorro/assets/svg/kosmorro-logo.svg View File


+ 42
- 0
_kosmorro/date.py View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3

import re

from dateutil.relativedelta import relativedelta
from datetime import date

from _kosmorro.i18n.utils import _


def parse_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]
)
) from error
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))

+ 15
- 0
_kosmorro/debug.py View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python3

from traceback import print_exc

show_debug_messages = False


def debug_print(what):
if not show_debug_messages:
return

if isinstance(what, Exception):
print_exc(what)
else:
print("[DEBUG] %s" % what)

+ 527
- 0
_kosmorro/dumper.py View File

@@ -0,0 +1,527 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from abc import ABC, abstractmethod
import datetime
import json
import os
import tempfile
import subprocess
import shutil
from pathlib import Path
from tabulate import tabulate
from termcolor import colored

from kosmorrolib import AsterEphemerides, Event
from kosmorrolib.model import ASTERS, MoonPhase

from .i18n.utils import _, FULL_DATE_FORMAT, SHORT_DATETIME_FORMAT, TIME_FORMAT
from .i18n import strings
from .__version__ import __version__ as version
from .exceptions import CompileError
from .debug import debug_print


class Dumper(ABC):
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

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

if capitalized:
return "".join([date[0].upper(), date[1:]])

return date

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

@abstractmethod
def to_string(self):
pass

@staticmethod
def is_file_output_needed() -> bool:
return False


class JsonDumper(Dumper):
def to_string(self):
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,
)


class TextDumper(Dumper):
def to_string(self):
text = [self.style(self.get_date_as_string(capitalized=True), "h1")]

if self.ephemerides is not None:
text.append(self.stringify_ephemerides())

text.append(self.get_moon(self.moon_phase))

if len(self.events) > 0:
text.append(
"\n".join(
[
self.style(_("Expected events:"), "h2"),
self.get_events(self.events),
]
)
)

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

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

def style(self, text: str, tag: str) -> str:
if not self.with_colors:
return text

styles = {
"h1": lambda t: colored(t, "yellow", attrs=["bold"]),
"h2": lambda t: colored(t, "magenta", attrs=["bold"]),
"th": lambda t: colored(t, "white", attrs=["bold"]),
"strong": lambda t: colored(t, attrs=["bold"]),
"em": lambda t: colored(t, attrs=["dark"]),
}

return styles[tag](text)

def stringify_ephemerides(self) -> str:
data = []

for ephemeris in self.ephemerides:
name = self.style(strings.from_object(ephemeris.object.identifier), "th")

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 ephemeris.culmination_time is not None:
time_fmt = (
TIME_FORMAT
if ephemeris.culmination_time.day == self.date.day
else SHORT_DATETIME_FORMAT
)
planet_culmination = ephemeris.culmination_time.strftime(time_fmt)
else:
planet_culmination = "-"

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 = "-"

data.append([name, planet_rise, planet_culmination, planet_set])

return tabulate(
data,
headers=[
self.style(_("Object"), "th"),
self.style(_("Rise time"), "th"),
self.style(_("Culmination time"), "th"),
self.style(_("Set time"), "th"),
],
tablefmt="simple",
stralign="center",
colalign=("left",),
)

def get_events(self, events: [Event]) -> str:
def get_event_description(ev: Event):
description = strings.from_event(ev)

if ev.details is not None:
description += " ({:s})".format(ev.details)
return description

data = []

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

return tabulate(data, tablefmt="plain", stralign="left")

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

current_moon_phase = " ".join(
[
self.style(_("Moon phase:"), "strong"),
strings.from_moon_phase(moon_phase.phase_type),
]
)
new_moon_phase = _(
"{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}"
).format(
next_moon_phase=_(strings.from_moon_phase(moon_phase.get_next_phase())),
next_moon_phase_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT),
next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_FORMAT),
)

return "\n".join([current_moon_phase, new_moon_phase])


class _LatexDumper(Dumper):
def to_string(self):
template_path = os.path.join(
os.path.abspath(os.path.dirname(__file__)), "assets", "pdf", "template.tex"
)

with open(template_path, mode="r") as file:
template = file.read()

return self._make_document(template)

def _make_document(self, template: str) -> str:
kosmorro_logo_path = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
"assets",
"png",
"kosmorro-logo.png",
)

moon_phase_graphics = os.path.join(
os.path.abspath(os.path.dirname(__file__)),
"assets",
"moonphases",
"png",
".".join(
[self.moon_phase.phase_type.name.lower().replace("_", "-"), "png"]
),
)

document = template

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: str, kosmorro_logo_path: str, moon_phase_graphics: str
) -> str:
document = document.replace("+++KOSMORRO-VERSION+++", version)
document = document.replace("+++KOSMORRO-LOGO+++", kosmorro_logo_path)
document = document.replace("+++DOCUMENT-TITLE+++", _("A Summary of your Sky"))
document = document.replace(
"+++DOCUMENT-DATE+++", self.get_date_as_string(capitalized=True)
)
document = document.replace(
"+++INTRODUCTION+++",
"\n\n".join(
[
_(
"This document summarizes the ephemerides and the events of {date}. "
"It aims to help you to prepare your observation session. "
"All the hours are given in {timezone}."
).format(
date=self.get_date_as_string(),
timezone="UTC+%d" % self.timezone
if self.timezone != 0
else "UTC",
),
_(
"Don't forget to check the weather forecast before you go out with your equipment."
),
]
),
)
document = document.replace(
"+++SECTION-EPHEMERIDES+++", _("Ephemerides of the day")
)
document = document.replace("+++EPHEMERIDES-OBJECT+++", _("Object"))
document = document.replace("+++EPHEMERIDES-RISE-TIME+++", _("Rise time"))
document = document.replace(
"+++EPHEMERIDES-CULMINATION-TIME+++", _("Culmination time")
)
document = document.replace("+++EPHEMERIDES-SET-TIME+++", _("Set time"))
document = document.replace("+++EPHEMERIDES+++", self._make_ephemerides())
document = document.replace("+++GRAPH_LABEL_HOURS+++", _("hours"))
document = document.replace("+++MOON-PHASE-GRAPHICS+++", moon_phase_graphics)
document = document.replace("+++CURRENT-MOON-PHASE-TITLE+++", _("Moon phase:"))
document = document.replace(
"+++CURRENT-MOON-PHASE+++",
strings.from_moon_phase(self.moon_phase.phase_type),
)
document = document.replace("+++SECTION-EVENTS+++", _("Expected events"))
document = document.replace("+++EVENTS+++", self._make_events())

for aster in ASTERS:
document = document.replace(
"+++ASTER_%s+++" % aster.skyfield_name.upper().split(" ")[0],
strings.from_object(aster.identifier),
)

return document

def _make_ephemerides(self) -> str:
latex = []
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}"
% (
strings.from_object(ephemeris.object.identifier),
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)

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

for event in self.events:
print(event)
latex.append(
r"\event{%s}{%s}"
% (event.start_time.strftime(TIME_FORMAT), strings.from_event(event))
)

return "".join(latex)

@staticmethod
def _remove_section(document: str, section: str):
begin_section_tag = "%%%%%% BEGIN-%s-SECTION" % section.upper()
end_section_tag = "%%%%%% END-%s-SECTION" % section.upper()

document = document.split("\n")
new_document = []

ignore_line = False
for line in document:
if begin_section_tag in line or end_section_tag in line:
ignore_line = not ignore_line
continue
if ignore_line:
continue
new_document.append(line)

return "\n".join(new_document)


class PdfDumper(Dumper):
def to_string(self):
try:
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 as error:
raise error
# raise UnavailableFeatureError(
# _(
# "Building PDFs was not possible, because some dependencies are not"
# " installed.\nPlease look at the documentation at http://kosmorro.space "
# "for more information."
# )
# ) from error

@staticmethod
def is_file_output_needed() -> bool:
return True

@staticmethod
def _compile(latex_input) -> bytes:
package = str(Path(__file__).parent.absolute()) + "/assets/pdf/kosmorro.sty"

with tempfile.TemporaryDirectory() as tempdir:
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
shutil.copy(package, tempdir)

with open("%s/%s.tex" % (tempdir, timestamp), "w") as texfile:
texfile.write(latex_input)

os.chdir(tempdir)
debug_print("LaTeX content:\n%s" % latex_input)

try:
subprocess.run(
["pdflatex", "-interaction", "nonstopmode", "%s.tex" % timestamp],
capture_output=True,
check=True,
)
except FileNotFoundError as error:
raise RuntimeError("pdflatex is not installed.") from error
except subprocess.CalledProcessError as error:
with open("/tmp/kosmorro-%s.log" % timestamp, "wb") as file:
file.write(error.stdout)

raise CompileError(
_(
"An error occured during the compilation of the PDF.\n"
"Please open an issue at https://github.com/Kosmorro/kosmorro/issues and share "
"the content of the log file at /tmp/kosmorro-%s.log"
% timestamp
)
) from error

with open("%s.pdf" % timestamp, "rb") as pdffile:
return bytes(pdffile.read())

kosmorrolib/enum.py → _kosmorro/environment.py View File

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

from enum import Enum
from .i18n import _


class MoonPhaseType(Enum):
NEW_MOON = _('New Moon')
WAXING_CRESCENT = _('Waxing crescent')
FIRST_QUARTER = _('First Quarter')
WAXING_GIBBOUS = _('Waxing gibbous')
FULL_MOON = _('Full Moon')
WANING_GIBBOUS = _('Waning gibbous')
LAST_QUARTER = _('Last Quarter')
WANING_CRESCENT = _('Waning crescent')


class EventType(Enum):
OPPOSITION = _('%s is in opposition')
CONJUNCTION = _('%s and %s are in conjunction')
OCCULTATION = _('%s occults %s')
MAXIMAL_ELONGATION = _("%s's largest elongation")
MOON_PERIGEE = _("%s is at its perigee")
MOON_APOGEE = _("%s is at its apogee")
import os
import re
from pathlib import Path

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


class Environment:
def __init__(self):
self._vars = {}

def __set__(self, key, value):
self._vars[key] = value

def __getattr__(self, key):
return self._vars[key] if key in self._vars else None

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

def __len__(self):
return len(self._vars)


def get_env_vars() -> Environment:
environment = Environment()

for var in os.environ:
if not re.search("^KOSMORRO_", var):
continue

[_, env] = var.split("_", 1)
environment.__set__(env.lower(), os.getenv(var))

return environment

kosmorrolib/exceptions.py → _kosmorro/exceptions.py View File

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

from datetime import date
from .i18n import _, SHORT_DATE_FORMAT
from _kosmorro.i18n.utils import _, SHORT_DATE_FORMAT


class UnavailableFeatureError(RuntimeError):
@@ -31,9 +31,12 @@ class OutOfRangeDateError(RuntimeError):
super().__init__()
self.min_date = min_date
self.max_date = max_date
self.msg = _('The date must be between {minimum_date}'
' and {maximum_date}').format(minimum_date=min_date.strftime(SHORT_DATE_FORMAT),
maximum_date=max_date.strftime(SHORT_DATE_FORMAT))
self.msg = _(
"The date must be between {minimum_date}" " and {maximum_date}"
).format(
minimum_date=min_date.strftime(SHORT_DATE_FORMAT),
maximum_date=max_date.strftime(SHORT_DATE_FORMAT),
)


class CompileError(RuntimeError):

+ 0
- 0
_kosmorro/i18n/__init__.py View File


+ 56
- 0
_kosmorro/i18n/strings.py View File

@@ -0,0 +1,56 @@
#!/usr/bin/env python3

from .utils import _

from kosmorrolib import EventType, MoonPhaseType, ObjectIdentifier, Event


def from_event(event: Event) -> str:
return {
EventType.OPPOSITION: _("%s is in opposition")
% (from_object(event.objects[0].identifier)),
EventType.CONJUNCTION: _("%s and %s are in conjunction")
% (
from_object(event.objects[0].identifier),
from_object(event.objects[1].identifier),
),
EventType.OCCULTATION: _("%s occults %s")
% (
from_object(event.objects[0].identifier),
from_object(event.objects[1].identifier),
),
EventType.MAXIMAL_ELONGATION: _("%s's largest elongation")
% (from_object(event.objects[0].identifier)),
EventType.MOON_PERIGEE: _("%s is at its perigee")
% (from_object(event.objects[0].identifier)),
EventType.MOON_APOGEE: _("%s is at its apogee")
% (from_object(event.objects[0].identifier)),
}.get(event.event_type)


def from_moon_phase(moon_phase: MoonPhaseType) -> str:
return {
MoonPhaseType.NEW_MOON: _("New Moon"),
MoonPhaseType.WAXING_CRESCENT: _("Waxing Crescent"),
MoonPhaseType.FIRST_QUARTER: _("First Quarter"),
MoonPhaseType.WAXING_GIBBOUS: _("Waxing Gibbous"),
MoonPhaseType.FULL_MOON: _("Full Moon"),
MoonPhaseType.WANING_GIBBOUS: _("Waning Gibbous"),
MoonPhaseType.LAST_QUARTER: _("Last Quarter"),
MoonPhaseType.WANING_CRESCENT: _("Waning Crescent"),
}.get(moon_phase, _("Unknown phase"))


def from_object(identifier: ObjectIdentifier) -> str:
return {
ObjectIdentifier.SUN: _("Sun"),
ObjectIdentifier.MOON: _("Moon"),
ObjectIdentifier.MERCURY: _("Mercury"),
ObjectIdentifier.VENUS: _("Venus"),
ObjectIdentifier.MARS: _("Mars"),
ObjectIdentifier.JUPITER: _("Jupiter"),
ObjectIdentifier.SATURN: _("Saturn"),
ObjectIdentifier.URANUS: _("Uranus"),
ObjectIdentifier.NEPTUNE: _("Neptune"),
ObjectIdentifier.PLUTO: _("Pluto"),
}.get(identifier, _("Unknown object"))

kosmorrolib/i18n.py → _kosmorro/i18n/utils.py View File

@@ -19,17 +19,21 @@
import gettext
import os

_LOCALE_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locales')
_TRANSLATION = gettext.translation('messages', localedir=_LOCALE_DIR, fallback=True)
_LOCALE_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "../locales")
_TRANSLATION = gettext.translation("messages", localedir=_LOCALE_DIR, fallback=True)

_ = _TRANSLATION.gettext

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


def ngettext(msgid1, msgid2, number):

kosmorrolib/locales/de/LC_MESSAGES/messages.po → _kosmorro/locales/de/LC_MESSAGES/messages.po View File


kosmorrolib/locales/es/LC_MESSAGES/messages.po → _kosmorro/locales/es/LC_MESSAGES/messages.po View File


kosmorrolib/locales/fr/LC_MESSAGES/messages.po → _kosmorro/locales/fr/LC_MESSAGES/messages.po View File


kosmorrolib/locales/messages.pot → _kosmorro/locales/messages.pot View File

@@ -8,143 +8,95 @@ msgid ""
msgstr ""
"Project-Id-Version: kosmorro 0.9.0\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-01-31 10:15+0100\n"
"POT-Creation-Date: 2021-05-08 17:53+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"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n"
"Generated-By: Babel 2.9.1\n"

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

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

#: kosmorrolib/data.py:197
msgid "Sun"
msgstr ""

#: kosmorrolib/data.py:198
msgid "Moon"
msgstr ""

#: kosmorrolib/data.py:199
msgid "Mercury"
msgstr ""

#: kosmorrolib/data.py:200
msgid "Venus"
msgstr ""

#: kosmorrolib/data.py:201
msgid "Mars"
msgstr ""

#: kosmorrolib/data.py:202
msgid "Jupiter"
msgstr ""

#: kosmorrolib/data.py:203
msgid "Saturn"
msgstr ""

#: kosmorrolib/data.py:204
msgid "Uranus"
msgstr ""

#: kosmorrolib/data.py:205
msgid "Neptune"
msgstr ""

#: kosmorrolib/data.py:206
msgid "Pluto"
msgstr ""

#: kosmorrolib/dumper.py:87
#: _kosmorro/dumper.py:106
msgid "Expected events:"
msgstr ""

#: kosmorrolib/dumper.py:91
#: _kosmorro/dumper.py:113
msgid "Note: All the hours are given in UTC."
msgstr ""

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

#: kosmorrolib/dumper.py:142 kosmorrolib/dumper.py:226
#: _kosmorro/dumper.py:184 _kosmorro/dumper.py:320
msgid "Object"
msgstr ""

#: kosmorrolib/dumper.py:143 kosmorrolib/dumper.py:227
#: _kosmorro/dumper.py:185 _kosmorro/dumper.py:321
msgid "Rise time"
msgstr ""

#: kosmorrolib/dumper.py:144 kosmorrolib/dumper.py:228
#: _kosmorro/dumper.py:186 _kosmorro/dumper.py:323
msgid "Culmination time"
msgstr ""

#: kosmorrolib/dumper.py:145 kosmorrolib/dumper.py:229
#: _kosmorro/dumper.py:187 _kosmorro/dumper.py:325
msgid "Set time"
msgstr ""

#: kosmorrolib/dumper.py:160
#: _kosmorro/dumper.py:221
msgid "Moon phase is unavailable for this date."
msgstr ""

#: kosmorrolib/dumper.py:162 kosmorrolib/dumper.py:233
#: _kosmorro/dumper.py:225 _kosmorro/dumper.py:329
msgid "Moon phase:"
msgstr ""

#: kosmorrolib/dumper.py:163
#: _kosmorro/dumper.py:229
msgid "{next_moon_phase} on {next_moon_phase_date} at {next_moon_phase_time}"
msgstr ""

#: kosmorrolib/dumper.py:213
#: _kosmorro/dumper.py:293
msgid "A Summary of your Sky"
msgstr ""

#: kosmorrolib/dumper.py:217
#: _kosmorro/dumper.py:301
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:223
#: _kosmorro/dumper.py:311
msgid ""
"Don't forget to check the weather forecast before you go out with your "
"equipment."
msgstr ""

#: kosmorrolib/dumper.py:225
#: _kosmorro/dumper.py:318
msgid "Ephemerides of the day"
msgstr ""

#: kosmorrolib/dumper.py:231
#: _kosmorro/dumper.py:327
msgid "hours"
msgstr ""

#: kosmorrolib/dumper.py:235
#: _kosmorro/dumper.py:334
msgid "Expected events"
msgstr ""

#: kosmorrolib/dumper.py:351
msgid ""
"Building PDFs was not possible, because some dependencies are not "
"installed.\n"
"Please look at the documentation at http://kosmorro.space for more "
"information."
msgstr ""

#: kosmorrolib/dumper.py:380
#: _kosmorro/dumper.py:518
#, python-format
msgid ""
"An error occured during the compilation of the PDF.\n"
@@ -152,188 +104,240 @@ msgid ""
"share the content of the log file at /tmp/kosmorro-%s.log"
msgstr ""

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

#: kosmorrolib/enum.py:25
msgid "Waxing crescent"
#: _kosmorro/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/enum.py:26
msgid "First Quarter"
#: _kosmorro/main.py:70
msgid ""
"PDF output will not contain the ephemerides, because you didn't provide "
"the observation coordinate."
msgstr ""

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

#: kosmorrolib/enum.py:27
msgid "Waxing gibbous"
#: _kosmorro/main.py:129
msgid "Selected output format needs an output file (--output)."
msgstr ""

#: kosmorrolib/enum.py:28
msgid "Full Moon"
#: _kosmorro/main.py:157
msgid "Moon phase can only be displayed between {min_date} and {max_date}"
msgstr ""

#: kosmorrolib/enum.py:29
msgid "Waning gibbous"
#: _kosmorro/main.py:194
msgid "Running on Python {python_version} with Kosmorrolib v{kosmorrolib_version}"
msgstr ""

#: kosmorrolib/enum.py:30
msgid "Last Quarter"
#: _kosmorro/main.py:204
msgid "Do you really want to clear Kosmorro's cache? [yN] "
msgstr ""

#: _kosmorro/main.py:212
msgid "Answer did not match expected options, cache not cleared."
msgstr ""

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

#: _kosmorro/main.py:226
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 ""

#: _kosmorro/main.py:240
msgid "Show the program version"
msgstr ""

#: _kosmorro/main.py:248
msgid "Delete all the files Kosmorro stored in the cache."
msgstr ""

#: _kosmorro/main.py:256
msgid "The format under which the information have to be output"
msgstr ""

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

#: kosmorrolib/enum.py:31
msgid "Waning crescent"
#: _kosmorro/main.py:273
msgid ""
"The observer's longitude on Earth. Can also be set in the "
"KOSMORRO_LONGITUDE environment variable."
msgstr ""

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

#: kosmorrolib/enum.py:35
#: _kosmorro/main.py:294
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 ""

#: _kosmorro/main.py:303
msgid "Disable the colors in the console."
msgstr ""

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

#: _kosmorro/main.py:319
msgid ""
"Do not generate a graph to represent the rise and set times in the PDF "
"format."
msgstr ""

#: _kosmorro/main.py:327
msgid "Show debugging messages"
msgstr ""

#: _kosmorro/i18n/strings.py:10
#, python-format
msgid "%s is in opposition"
msgstr ""

#: kosmorrolib/enum.py:36
#: _kosmorro/i18n/strings.py:12
#, python-format
msgid "%s and %s are in conjunction"
msgstr ""

#: kosmorrolib/enum.py:37
#: _kosmorro/i18n/strings.py:17
#, python-format
msgid "%s occults %s"
msgstr ""

#: kosmorrolib/enum.py:38
#: _kosmorro/i18n/strings.py:22
#, python-format
msgid "%s's largest elongation"
msgstr ""

#: kosmorrolib/enum.py:39
#: _kosmorro/i18n/strings.py:24
#, python-format
msgid "%s is at its perigee"
msgstr ""

#: kosmorrolib/enum.py:40
#: _kosmorro/i18n/strings.py:26
#, python-format
msgid "%s is at its apogee"
msgstr ""

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

#: kosmorrolib/i18n.py:27
msgid "{day_of_week} {month} {day_number}, {year}"
#: _kosmorro/i18n/strings.py:33
msgid "New Moon"
msgstr ""

#: kosmorrolib/i18n.py:29
msgid "{month} {day_number}, {hours}:{minutes}"
#: _kosmorro/i18n/strings.py:34
msgid "Waxing Crescent"
msgstr ""

#: kosmorrolib/i18n.py:31
msgid "{month} {day_number}, {year}"
#: _kosmorro/i18n/strings.py:35
msgid "First Quarter"
msgstr ""

#: kosmorrolib/i18n.py:32
msgid "{hours}:{minutes}"
#: _kosmorro/i18n/strings.py:36
msgid "Waxing Gibbous"
msgstr ""

#: 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."
#: _kosmorro/i18n/strings.py:37
msgid "Full Moon"
msgstr ""

#: kosmorrolib/main.py:65
msgid ""
"PDF output will not contain the ephemerides, because you didn't provide "
"the observation coordinate."
#: _kosmorro/i18n/strings.py:38
msgid "Waning Gibbous"
msgstr ""

#: kosmorrolib/main.py:93
msgid "Could not save the output in \"{path}\": {error}"
#: _kosmorro/i18n/strings.py:39
msgid "Last Quarter"
msgstr ""

#: kosmorrolib/main.py:100
msgid "Selected output format needs an output file (--output)."
#: _kosmorro/i18n/strings.py:40
msgid "Waning Crescent"
msgstr ""

#: kosmorrolib/main.py:117
msgid "Moon phase can only be displayed between {min_date} and {max_date}"
#: _kosmorro/i18n/strings.py:41
msgid "Unknown phase"
msgstr ""

#: kosmorrolib/main.py:139
msgid "Running on Python {python_version}"
#: _kosmorro/i18n/strings.py:46
msgid "Sun"
msgstr ""

#: kosmorrolib/main.py:145
msgid "Do you really want to clear Kosmorro's cache? [yN] "
#: _kosmorro/i18n/strings.py:47
msgid "Moon"
msgstr ""

#: kosmorrolib/main.py:152
msgid "Answer did not match expected options, cache not cleared."
#: _kosmorro/i18n/strings.py:48
msgid "Mercury"
msgstr ""

#: kosmorrolib/main.py:161
msgid ""
"Compute the ephemerides and the events for a given date, at a given "
"position on Earth."
#: _kosmorro/i18n/strings.py:49
msgid "Venus"
msgstr ""

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

#: kosmorrolib/main.py:168
msgid "Show the program version"
#: _kosmorro/i18n/strings.py:51
msgid "Jupiter"
msgstr ""

#: kosmorrolib/main.py:170
msgid "Delete all the files Kosmorro stored in the cache."
#: _kosmorro/i18n/strings.py:52
msgid "Saturn"
msgstr ""

#: kosmorrolib/main.py:172
msgid "The format under which the information have to be output"
#: _kosmorro/i18n/strings.py:53
msgid "Uranus"
msgstr ""

#: kosmorrolib/main.py:174
msgid ""
"The observer's latitude on Earth. Can also be set in the "
"KOSMORRO_LATITUDE environment variable."
#: _kosmorro/i18n/strings.py:54
msgid "Neptune"
msgstr ""

#: kosmorrolib/main.py:177
msgid ""
"The observer's longitude on Earth. Can also be set in the "
"KOSMORRO_LONGITUDE environment variable."
#: _kosmorro/i18n/strings.py:55
msgid "Pluto"
msgstr ""

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

#: kosmorrolib/main.py:185
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."
#: _kosmorro/i18n/utils.py:27
msgid "{day_of_week} {month} {day_number}, {year}"
msgstr ""

#: kosmorrolib/main.py:188
msgid "Disable the colors in the console."
#: _kosmorro/i18n/utils.py:30
msgid "{month} {day_number}, {hours}:{minutes}"
msgstr ""

#: kosmorrolib/main.py:190
msgid ""
"A file to export the output to. If not given, the standard output is "
"used. This argument is needed for PDF format."
#: _kosmorro/i18n/utils.py:33
msgid "{month} {day_number}, {year}"
msgstr ""

#: kosmorrolib/main.py:193
msgid ""
"Do not generate a graph to represent the rise and set times in the PDF "
"format."
#: _kosmorro/i18n/utils.py:36
msgid "{hours}:{minutes}"
msgstr ""


kosmorrolib/locales/nb_NO/LC_MESSAGES/messages.po → _kosmorro/locales/nb_NO/LC_MESSAGES/messages.po View File


kosmorrolib/locales/nl/LC_MESSAGES/messages.po → _kosmorro/locales/nl/LC_MESSAGES/messages.po View File


kosmorrolib/locales/ru/LC_MESSAGES/messages.po → _kosmorro/locales/ru/LC_MESSAGES/messages.po View File


+ 330
- 0
_kosmorro/main.py View File

@@ -0,0 +1,330 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import argparse
import locale
import re
import sys

from kosmorrolib import Position, get_ephemerides, get_events, get_moon_phase
from kosmorrolib.__version__ import __version__ as kosmorrolib_version
from datetime import date
from termcolor import colored

from . import dumper, environment, debug
from .date import parse_date
from .__version__ import __version__ as kosmorro_version
from .exceptions import UnavailableFeatureError, OutOfRangeDateError
from _kosmorro.i18n.utils import _


def main():
env_vars = environment.get_env_vars()
output_formats = get_dumpers()
args = get_args(list(output_formats.keys()))
debug.show_debug_messages = args.show_debug_messages
output_format = args.format

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

try:
compute_date = parse_date(args.date)
except ValueError as error:
print(colored(error.args[0], color="red", attrs=["bold"]))
return -1

position = None

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

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:
print()
print(
colored(
_(
"PDF output will not contain the ephemerides, because you didn't provide the observation "
"coordinate."
),
"yellow",
)
)

timezone = args.timezone

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

try:
output = get_information(
compute_date,
position,
timezone,
output_format,
args.colors,
args.show_graph,
)
except UnavailableFeatureError as error:
print(colored(error.msg, "red"))
debug.debug_print(error)
return 2
except OutOfRangeDateError as error:
print(colored(error.msg, "red"))
debug.debug_print(error)
return 1

if args.output is not None:
try:
pdf_content = output.to_string()
with open(args.output, "wb") as output_file:
output_file.write(pdf_content)
except UnavailableFeatureError as error:
print(colored(error.msg, "red"))
debug.debug_print(error)
return 2
except OSError as error:
print(
colored(
_('Could not save the output in "{path}": {error}').format(
path=args.output, error=error.strerror
),
"red",
)
)
debug.debug_print(error)

return 3
elif not output.is_file_output_needed():
print(output)
else:
print(
colored(
_("Selected output format needs an output file (--output)."),
color="red",
)
)
return 1

return 0


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

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

events_list = get_events(compute_date, timezone)

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


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


def output_version() -> bool:
python_version = "%d.%d.%d" % (
sys.version_info[0],
sys.version_info[1],
sys.version_info[2],
)
print("Kosmorro %s" % kosmorro_version)
print(
_(
"Running on Python {python_version} "
"with Kosmorrolib v{kosmorrolib_version}"
).format(python_version=python_version, kosmorrolib_version=kosmorrolib_version)
)

return True


def clear_cache() -> bool:
confirm = input(_("Do you really want to clear Kosmorro's cache? [yN] ")).upper()
if re.match(locale.nl_langinfo(locale.YESEXPR), confirm) is not None:
try:
environment.clear_cache()
except FileNotFoundError:
debug.debug_print("No cache found, nothing done.")
pass
elif confirm != "" and re.match(locale.nl_langinfo(locale.NOEXPR), confirm) is None:
print(_("Answer did not match expected options, cache not cleared."))
return False

return True


def get_args(output_formats: [str]):
today = date.today()

parser = argparse.ArgumentParser(
description=_(
"Compute the ephemerides and the events for a given date,"
" at a given position on Earth."
),
epilog=_(
"By default, only the events will be computed for today ({date}).\n"
"To compute also the ephemerides, latitude and longitude arguments"
" are needed."
).format(date=today.strftime(dumper.FULL_DATE_FORMAT)),
)

parser.add_argument(
"--version",
"-v",
dest="special_action",
action="store_const",
const=output_version,
default=None,
help=_("Show the program version"),
)
parser.add_argument(
"--clear-cache",
dest="special_action",
action="store_const",
const=clear_cache,
default=None,
help=_("Delete all the files Kosmorro stored in the cache."),
)
parser.add_argument(
"--format",
"-f",
type=str,
default=output_formats[0],
choices=output_formats,
help=_("The format under which the information have to be output"),
)
parser.add_argument(
"--latitude",
"-lat",
type=float,
default=None,
help=_(
"The observer's latitude on Earth. Can also be set in the KOSMORRO_LATITUDE environment "
"variable."
),
)
parser.add_argument(
"--longitude",
"-lon",
type=float,
default=None,
help=_(
"The observer's longitude on Earth. Can also be set in the KOSMORRO_LONGITUDE "
"environment variable."
),
)
parser.add_argument(
"--date",
"-d",
type=str,
default=today.strftime("%Y-%m-%d"),
help=_(
"The date for which the ephemerides must be computed (in the YYYY-MM-DD format), "
'or as an interval in the "[+-]YyMmDd" format (with Y, M, and D numbers). '
"Defaults to the current date ({default_date})"
).format(default_date=today.strftime("%Y-%m-%d")),
)
parser.add_argument(
"--timezone",
"-t",
type=int,
default=None,
help=_(
"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."
),
)
parser.add_argument(
"--no-colors",
dest="colors",
action="store_false",
help=_("Disable the colors in the console."),
)
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=_(
"Do not generate a graph to represent the rise and set times in the PDF format."
),
)
parser.add_argument(
"--debug",
dest="show_debug_messages",
action="store_true",
help=_("Show debugging messages"),
)

return parser.parse_args()

+ 3
- 3
kosmorro View File

@@ -18,11 +18,11 @@

import sys
import locale
from kosmorrolib.main import main
from _kosmorro.main import main

locale.setlocale(locale.LC_ALL, '')
locale.setlocale(locale.LC_ALL, "")

if __name__ == '__main__':
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:


+ 0
- 119
kosmorrolib/core.py View File

@@ -1,119 +0,0 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import os
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:
def __init__(self):
self._vars = {}

def __set__(self, key, value):
self._vars[key] = value

def __getattr__(self, key):
return self._vars[key] if key in self._vars else None

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

def __len__(self):
return len(self._vars)

def get_env() -> Environment:
environment = Environment()

for var in os.environ:
if not re.search('^KOSMORRO_', var):
continue

[_, env] = var.split('_', 1)
environment.__set__(env.lower(), os.getenv(var))

return environment

def get_loader():
return Loader(CACHE_FOLDER)


def get_timescale():
return get_loader().timescale()


def get_skf_objects():
return get_loader()('de421.bsp')


def get_iau2000b(time: Time):
return iau2000b(time.tt)


def clear_cache():
rmtree(CACHE_FOLDER)


def flatten_list(the_list: list):
new_list = []
for item in the_list:
if isinstance(item, list):
for item2 in flatten_list(item):
new_list.append(item2)
continue

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])) from error
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))

+ 0
- 224
kosmorrolib/data.py View File

@@ -1,224 +0,0 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

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

from numpy import pi, arcsin

from skyfield.api import Topos, Time
from skyfield.vectorlib import VectorSum as SkfPlanet

from .core import get_skf_objects
from .enum import MoonPhaseType, EventType
from .i18n import _


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


class MoonPhase(Serializable):
def __init__(self, phase_type: MoonPhaseType, time: datetime = None, next_phase_date: datetime = None):
self.phase_type = phase_type
self.time = time
self.next_phase_date = next_phase_date

def get_next_phase(self):
if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]:
return MoonPhaseType.FIRST_QUARTER
if self.phase_type in [MoonPhaseType.FIRST_QUARTER, MoonPhaseType.WAXING_GIBBOUS]:
return MoonPhaseType.FULL_MOON
if self.phase_type in [MoonPhaseType.FULL_MOON, MoonPhaseType.WANING_GIBBOUS]:
return MoonPhaseType.LAST_QUARTER

return MoonPhaseType.NEW_MOON

def serialize(self) -> dict:
return {
'phase': self.phase_type.name,
'time': self.time.isoformat() if self.time is not None else None,
'next': {
'phase': self.get_next_phase().name,
'time': self.next_phase_date.isoformat()
}
}


class Object(Serializable):
"""
An astronomical object.
"""

def __init__(self,
name: str,
skyfield_name: str,
radius: float = None):
"""
Initialize an astronomical object

:param str name: the official name of the object (may be internationalized)
:param str skyfield_name: the internal name of the object in Skyfield library
:param float radius: the radius (in km) of the object
:param AsterEphemerides ephemerides: the ephemerides associated to the object
"""
self.name = name
self.skyfield_name = skyfield_name
self.radius = radius

def __repr__(self):
return '<Object type=%s name=%s />' % (self.get_type(), self.name)

def get_skyfield_object(self) -> SkfPlanet:
return get_skf_objects()[self.skyfield_name]

@abstractmethod
def get_type(self) -> str:
pass

def get_apparent_radius(self, time: Time, from_place) -> float:
"""
Calculate the apparent radius, in degrees, of the object from the given place at a given time.
:param time:
:param from_place:
:return:
"""
if self.radius is None:
raise ValueError('Missing radius for %s object' % self.name)

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:
return 'star'


class Planet(Object):
def get_type(self) -> str:
return 'planet'


class DwarfPlanet(Planet):
def get_type(self) -> str:
return 'dwarf_planet'


class Satellite(Object):
def get_type(self) -> str:
return 'satellite'


class Event(Serializable):
def __init__(self, event_type: EventType, objects: [Object], start_time: datetime,
end_time: Union[datetime, None] = None, details: str = None):
self.event_type = event_type
self.objects = objects
self.start_time = start_time
self.end_time = end_time
self.details = details

def __repr__(self):
return '<Event type=%s objects=[%s] start=%s end=%s details=%s>' % (self.event_type,
self.objects,
self.start_time,
self.end_time,
self.details)

def get_description(self, show_details: bool = True) -> str:
description = self.event_type.value % self._get_objects_name()
if show_details and self.details is not None:
description += ' ({:s})'.format(self.details)
return description

def _get_objects_name(self):
if len(self.objects) == 1:
return self.objects[0].name

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.name,
'starts_at': self.start_time.isoformat(),
'ends_at': self.end_time.isoformat() if self.end_time is not None else None,
'details': self.details
}


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
}


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

ASTERS = [Star(_('Sun'), 'SUN', radius=696342),
Satellite(_('Moon'), 'MOON', radius=1737.4),
Planet(_('Mercury'), 'MERCURY', radius=2439.7),
Planet(_('Venus'), 'VENUS', radius=6051.8),
Planet(_('Mars'), 'MARS', radius=3396.2),
Planet(_('Jupiter'), 'JUPITER BARYCENTER', radius=71492),
Planet(_('Saturn'), 'SATURN BARYCENTER', radius=60268),
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

+ 0
- 28
kosmorrolib/dateutil.py View File

@@ -1,28 +0,0 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from datetime import datetime, timezone, timedelta


def translate_to_timezone(date: datetime, to_tz: int, from_tz: int = None):
if from_tz is not None:
source_tz = timezone(timedelta(hours=from_tz))
else:
source_tz = timezone.utc

return date.replace(tzinfo=source_tz).astimezone(tz=timezone(timedelta(hours=to_tz)))

+ 0
- 385
kosmorrolib/dumper.py View File

@@ -1,385 +0,0 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from abc import ABC, abstractmethod
import datetime
import json
import os
import tempfile
import subprocess
import shutil
from pathlib import Path
from tabulate import tabulate
from termcolor import colored

from .data import ASTERS, AsterEphemerides, MoonPhase, Event
from .i18n import _, FULL_DATE_FORMAT, SHORT_DATETIME_FORMAT, TIME_FORMAT
from .version import VERSION
from .exceptions import UnavailableFeatureError, CompileError


class Dumper(ABC):
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

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

if capitalized:
return ''.join([date[0].upper(), date[1:]])

return date

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

@abstractmethod
def to_string(self):
pass

@staticmethod
def is_file_output_needed() -> bool:
return False


class JsonDumper(Dumper):
def to_string(self):
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)


class TextDumper(Dumper):
def to_string(self):
text = [self.style(self.get_date_as_string(capitalized=True), 'h1')]

if self.ephemerides is not None:
text.append(self.stringify_ephemerides())

text.append(self.get_moon(self.moon_phase))

if len(self.events) > 0:
text.append('\n'.join([self.style(_('Expected events:'), 'h2'),
self.get_events(self.events)]))

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

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

def style(self, text: str, tag: str) -> str:
if not self.with_colors:
return text

styles = {
'h1': lambda t: colored(t, 'yellow', attrs=['bold']),
'h2': lambda t: colored(t, 'magenta', attrs=['bold']),
'th': lambda t: colored(t, 'white', attrs=['bold']),
'strong': lambda t: colored(t, attrs=['bold']),
'em': lambda t: colored(t, attrs=['dark'])
}

return styles[tag](text)

def stringify_ephemerides(self) -> str:
data = []

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

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 ephemeris.culmination_time is not None:
time_fmt = TIME_FORMAT if ephemeris.culmination_time.day == self.date.day \
else SHORT_DATETIME_FORMAT
planet_culmination = ephemeris.culmination_time.strftime(time_fmt)
else:
planet_culmination = '-'

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 = '-'

data.append([name, planet_rise, planet_culmination, planet_set])

return tabulate(data, headers=[self.style(_('Object'), 'th'),
self.style(_('Rise time'), 'th'),
self.style(_('Culmination time'), 'th'),
self.style(_('Set time'), 'th')],
tablefmt='simple', stralign='center', colalign=('left',))

def get_events(self, events: [Event]) -> str:
data = []

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

return tabulate(data, tablefmt='plain', stralign='left')

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

current_moon_phase = ' '.join([self.style(_('Moon phase:'), 'strong'), moon_phase.phase_type.value])
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().value,
next_moon_phase_date=moon_phase.next_phase_date.strftime(FULL_DATE_FORMAT),
next_moon_phase_time=moon_phase.next_phase_date.strftime(TIME_FORMAT)
)

return '\n'.join([current_moon_phase, new_moon_phase])


class _LatexDumper(Dumper):
def to_string(self):
template_path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'assets', 'pdf', 'template.tex')

with open(template_path, mode='r') as file:
template = file.read()

return self._make_document(template)

def _make_document(self, template: str) -> str:
kosmorro_logo_path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'assets', 'png', 'kosmorro-logo.png')

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

document = template

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) \
.replace('+++DOCUMENT-TITLE+++', _('A Summary of your Sky')) \
.replace('+++DOCUMENT-DATE+++', self.get_date_as_string(capitalized=True)) \
.replace('+++INTRODUCTION+++',
'\n\n'.join([
_("This document summarizes the ephemerides and the events of {date}. "
"It aims to help you to prepare your observation session. "
"All the hours are given in {timezone}.").format(
date=self.get_date_as_string(),
timezone='UTC+%d' % self.timezone if self.timezone != 0 else 'UTC'
),
_("Don't forget to check the weather forecast before you go out with your equipment.")
])) \
.replace('+++SECTION-EPHEMERIDES+++', _('Ephemerides of the day')) \
.replace('+++EPHEMERIDES-OBJECT+++', _('Object')) \
.replace('+++EPHEMERIDES-RISE-TIME+++', _('Rise time')) \
.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.moon_phase.phase_type.value) \
.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 = []
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)

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

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

return ''.join(latex)

@staticmethod
def _remove_section(document: str, section: str):
begin_section_tag = '%%%%%% BEGIN-%s-SECTION' % section.upper()
end_section_tag = '%%%%%% END-%s-SECTION' % section.upper()

document = document.split('\n')
new_document = []

ignore_line = False
for line in document:
if begin_section_tag in line or end_section_tag in line:
ignore_line = not ignore_line
continue
if ignore_line:
continue
new_document.append(line)

return '\n'.join(new_document)


class PdfDumper(Dumper):
def to_string(self):
try:
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 CompileError as error:
raise UnavailableFeatureError from error
except RuntimeError as error:
raise UnavailableFeatureError(_("Building PDFs was not possible, because some dependencies are not"
" installed.\nPlease look at the documentation at http://kosmorro.space "
"for more information.")) from error

@staticmethod
def is_file_output_needed() -> bool:
return True

@staticmethod
def _compile(latex_input) -> bytes:
package = str(Path(__file__).parent.absolute()) + '/assets/pdf/kosmorro.sty'

with tempfile.TemporaryDirectory() as tempdir:
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
shutil.copy(package, tempdir)

with open('%s/%s.tex' % (tempdir, timestamp), 'w') as texfile:
texfile.write(latex_input)

os.chdir(tempdir)
try:
subprocess.run(['pdflatex', '-interaction', 'nonstopmode', '%s.tex' % timestamp],
capture_output=True, check=True)
except FileNotFoundError as error:
raise RuntimeError('pdflatex is not installed.') from error
except subprocess.CalledProcessError as error:
with open('/tmp/kosmorro-%s.log' % timestamp, 'wb') as file:
file.write(error.stdout)

raise CompileError(_('An error occured during the compilation of the PDF.\n'
'Please open an issue at https://github.com/Kosmorro/kosmorro/issues and share '
'the content of the log file at /tmp/kosmorro-%s.log' % timestamp)) from error

with open('%s.pdf' % timestamp, 'rb') as pdffile:
return bytes(pdffile.read())

+ 0
- 163
kosmorrolib/ephemerides.py View File

@@ -1,163 +0,0 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import datetime
from typing import Union

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

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

RISEN_ANGLE = -0.8333


def _get_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)

phases = list(MoonPhaseType)
current_phase = None
current_phase_time = None
next_phase_time = None
i = 0

if len(times) == 0:
return None

for i, time in enumerate(times):
if now.utc_iso() <= time.utc_iso():
if vals[i] in [0, 2, 4, 6]:
if time.utc_datetime() < tomorrow.utc_datetime():
current_phase_time = time
current_phase = phases[vals[i]]
else:
i -= 1
current_phase_time = None
current_phase = phases[vals[i]]
else:
current_phase = phases[vals[i]]

break

for j in range(i + 1, len(times)):
if vals[j] in [0, 2, 4, 6]:
next_phase_time = times[j]
break

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


def get_moon_phase(compute_date: datetime.date, timezone: int = 0) -> MoonPhase:
earth = get_skf_objects()['earth']
moon = get_skf_objects()['moon']
sun = get_skf_objects()['sun']

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)

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)

try:
times, phase = find_discrete(time1, time2, moon_phase_at)
except EphemerisRangeError as error:
start = translate_to_timezone(error.start_time.utc_datetime(), timezone)
end = translate_to_timezone(error.end_time.utc_datetime(), timezone)

start = datetime.date(start.year, start.month, start.day) + datetime.timedelta(days=12)
end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12)

raise OutOfRangeDateError(start, end) from error

return _get_skyfield_to_moon_phase(times, phase, today)


def get_ephemerides(date: datetime.date, position: Position, timezone: int = 0) -> [AsterEphemerides]:
ephemerides = []

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

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

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

try:
for aster in ASTERS:
rise_times, arr = find_discrete(start_time, end_time, is_risen(aster))
try:
culmination_time, _ = find_maxima(start_time, end_time, f=get_angle(aster), epsilon=1./3600/24, num=12)
culmination_time = culmination_time[0] if len(culmination_time) > 0 else None
except ValueError:
culmination_time = None

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

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

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

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

ephemerides.append(AsterEphemerides(rise_time, culmination_time, set_time, aster=aster))
except EphemerisRangeError as error:
start = translate_to_timezone(error.start_time.utc_datetime(), timezone)
end = translate_to_timezone(error.end_time.utc_datetime(), timezone)

start = datetime.date(start.year, start.month, start.day + 1)
end = datetime.date(end.year, end.month, end.day - 1)

raise OutOfRangeDateError(start, end) from error

return ephemerides

+ 0
- 199
kosmorrolib/events.py View File

@@ -1,199 +0,0 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from datetime import date as date_type

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

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


def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Event]:
earth = get_skf_objects()['earth']
aster1 = None
aster2 = None

def is_in_conjunction(time: Time):
earth_pos = earth.at(time)
_, aster1_lon, _ = earth_pos.observe(aster1.get_skyfield_object()).apparent().ecliptic_latlon()
_, aster2_lon, _ = earth_pos.observe(aster2.get_skyfield_object()).apparent().ecliptic_latlon()

return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype('int8') == 0

is_in_conjunction.rough_period = 60.0

computed = []
conjunctions = []

for aster1 in ASTERS:
# Ignore the Sun
if isinstance(aster1, Star):
continue

for aster2 in ASTERS:
if isinstance(aster2, Star) or aster2 == aster1 or aster2 in computed:
continue

times, is_conjs = find_discrete(start_time, end_time, is_in_conjunction)

for i, time in enumerate(times):
if is_conjs[i]:
aster1_pos = (aster1.get_skyfield_object() - earth).at(time)
aster2_pos = (aster2.get_skyfield_object() - earth).at(time)
distance = aster1_pos.separation_from(aster2_pos).degrees

if distance - aster2.get_apparent_radius(time, earth) < aster1.get_apparent_radius(time, earth):
occulting_aster = [aster1,
aster2] if aster1_pos.distance().km < aster2_pos.distance().km else [aster2,
aster1]

conjunctions.append(Event(EventType.OCCULTATION, occulting_aster,
translate_to_timezone(time.utc_datetime(), timezone)))
else:
conjunctions.append(Event(EventType.CONJUNCTION, [aster1, aster2],
translate_to_timezone(time.utc_datetime(), timezone)))

computed.append(aster1)

return conjunctions


def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Event]:
earth = get_skf_objects()['earth']
sun = get_skf_objects()['sun']
aster = None

def is_oppositing(time: Time) -> [bool]:
earth_pos = earth.at(time)
sun_pos = earth_pos.observe(sun).apparent() # Never do this without eyes protection!
aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent()
_, lon1, _ = sun_pos.ecliptic_latlon()
_, lon2, _ = aster_pos.ecliptic_latlon()
return (lon1.degrees - lon2.degrees) > 180

is_oppositing.rough_period = 1.0
events = []

for aster in ASTERS:
if not isinstance(aster, Planet) or aster.skyfield_name in ['MERCURY', 'VENUS']:
continue

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

return events


def _search_maximal_elongations(start_time: Time, end_time: Time, timezone: int) -> [Event]:
earth = get_skf_objects()['earth']
sun = get_skf_objects()['sun']
aster = None

def get_elongation(time: Time):
sun_pos = (sun - earth).at(time)
aster_pos = (aster.get_skyfield_object() - earth).at(time)
separation = sun_pos.separation_from(aster_pos)
return separation.degrees

get_elongation.rough_period = 1.0

events = []

for aster in ASTERS:
if aster.skyfield_name not in ['MERCURY', 'VENUS']:
continue

times, elongations = find_maxima(start_time, end_time, f=get_elongation, epsilon=1./24/3600, num=12)

for i, time in enumerate(times):
elongation = elongations[i]
events.append(Event(EventType.MAXIMAL_ELONGATION,
[aster],
translate_to_timezone(time.utc_datetime(), timezone),
details='{:.3n}°'.format(elongation)))

return events


def _get_moon_distance():
earth = get_skf_objects()['earth']
moon = get_skf_objects()['moon']

def get_distance(time: Time):
earth_pos = earth.at(time)
moon_pos = earth_pos.observe(moon).apparent()

return moon_pos.distance().au

get_distance.rough_period = 1.0

return get_distance


def _search_moon_apogee(start_time: Time, end_time: Time, timezone: int) -> [Event]:
moon = ASTERS[1]
events = []

times, _ = find_maxima(start_time, end_time, f=_get_moon_distance(), epsilon=1./24/60)

for time in times:
events.append(Event(EventType.MOON_APOGEE, [moon], translate_to_timezone(time.utc_datetime(), timezone)))

return events


def _search_moon_perigee(start_time: Time, end_time: Time, timezone: int) -> [Event]:
moon = ASTERS[1]
events = []

times, _ = find_minima(start_time, end_time, f=_get_moon_distance(), epsilon=1./24/60)

for time in times:
events.append(Event(EventType.MOON_PERIGEE, [moon], translate_to_timezone(time.utc_datetime(), timezone)))

return events


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

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

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

raise OutOfRangeDateError(start_date, end_date) from error

+ 0
- 195
kosmorrolib/main.py View File

@@ -1,195 +0,0 @@
#!/usr/bin/env python3

# Kosmorro - Compute The Next Ephemerides
# Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import argparse
import locale
import re
import sys

from datetime import date
from termcolor import colored

from . import dumper
from . import core
from . import events

from .data import Position, EARTH
from .exceptions import UnavailableFeatureError, OutOfRangeDateError
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 = core.get_date(args.date)
except ValueError as error:
print(colored(error.args[0], color='red', attrs=['bold']))
return -1

position = None

if args.latitude is not None or args.longitude is not None:
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), EARTH)

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:
print()
print(colored(_("PDF output will not contain the ephemerides, because you didn't provide the observation "
"coordinate."), 'yellow'))

timezone = args.timezone

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

try:
output = get_information(compute_date, position, timezone, output_format,
args.colors, args.show_graph)
except UnavailableFeatureError as error:
print(colored(error.msg, 'red'))
return 2
except OutOfRangeDateError as error:
print(colored(error.msg, 'red'))
return 1

if args.output is not None:
try:
with open(args.output, 'wb') as output_file:
output_file.write(output.to_string())
except UnavailableFeatureError as error:
print(colored(error.msg, 'red'))
return 2
except OSError as error:
print(colored(_('Could not save the output in "{path}": {error}').format(path=args.output,
error=error.strerror),
'red'))
return 3
elif not output.is_file_output_needed():
print(output)
else:
print(colored(_('Selected output format needs an output file (--output).'), color='red'))
return 1

return 0


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

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

events_list = events.search_events(compute_date, timezone)

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


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


def output_version() -> bool:
python_version = '%d.%d.%d' % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
print('Kosmorro %s' % VERSION)
print(_('Running on Python {python_version}').format(python_version=python_version))

return True


def clear_cache() -> bool:
confirm = input(_("Do you really want to clear Kosmorro's cache? [yN] ")).upper()
if re.match(locale.nl_langinfo(locale.YESEXPR), confirm) is not None:
try:
core.clear_cache()
except FileNotFoundError:
pass
elif confirm != '' and re.match(locale.nl_langinfo(locale.NOEXPR), confirm) is None:
print(_('Answer did not match expected options, cache not cleared.'))
return False

return True


def get_args(output_formats: [str]):
today = date.today()

parser = argparse.ArgumentParser(description=_('Compute the ephemerides and the events for a given date,'
' at a given position on Earth.'),
epilog=_('By default, only the events will be computed for today ({date}).\n'
'To compute also the ephemerides, latitude and longitude arguments'
' are needed.').format(date=today.strftime(dumper.FULL_DATE_FORMAT)))

parser.add_argument('--version', '-v', dest='special_action', action='store_const', const=output_version,
default=None, help=_('Show the program version'))
parser.add_argument('--clear-cache', dest='special_action', action='store_const', const=clear_cache, default=None,
help=_('Delete all the files Kosmorro stored in the cache.'))
parser.add_argument('--format', '-f', type=str, default=output_formats[0], choices=output_formats,
help=_('The format under which the information have to be output'))
parser.add_argument('--latitude', '-lat', type=float, default=None,
help=_("The observer's latitude on Earth. Can also be set in the KOSMORRO_LATITUDE environment "
"variable."))
parser.add_argument('--longitude', '-lon', type=float, default=None,
help=_("The observer's longitude on Earth. Can also be set in the KOSMORRO_LONGITUDE "
"environment variable."))
parser.add_argument('--date', '-d', type=str, default=today.strftime('%Y-%m-%d'),
help=_('The date for which the ephemerides must be computed (in the YYYY-MM-DD format), '
'or as an interval in the "[+-]YyMmDd" format (with Y, M, and D numbers). '
'Defaults to the current date ({default_date})').format(
default_date=today.strftime('%Y-%m-%d')))
parser.add_argument('--timezone', '-t', type=int, default=None,
help=_('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.'))
parser.add_argument('--no-colors', dest='colors', action='store_false',
help=_('Disable the colors in the console.'))
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=_('Do not generate a graph to represent the rise and set times in the PDF format.'))

return parser.parse_args()

+ 26
- 21
setup.py View File

@@ -19,36 +19,41 @@
import pathlib
from setuptools import setup, find_packages

from kosmorrolib.version import VERSION
from _kosmorro.__version__ import __version__

HERE = pathlib.Path(__file__).parent
README = (HERE / 'README.md').read_text()
README = (HERE / "README.md").read_text()

setup(
name='kosmorro',
version=VERSION,
author='Jérôme Deuchnord',
author_email='jerome@deuchnord.fr',
url='http://kosmorro.space',
license='AGPL-3.0',
description='A program that computes the ephemerides.',
name="kosmorro",
version=__version__,
author="Jérôme Deuchnord",
author_email="jerome@deuchnord.fr",
url="http://kosmorro.space",
license="AGPL-3.0",
description="A program that computes the ephemerides.",
long_description=README,
long_description_content_type='text/markdown',
keywords='kosmorro astronomy ephemerides ephemeris',
long_description_content_type="text/markdown",
keywords="kosmorro astronomy ephemerides ephemeris",
packages=find_packages(),
scripts=['kosmorro'],
scripts=["kosmorro"],
include_package_data=True,
data_files=[
('man/man1', ['manpage/kosmorro.1']),
('man/man7', ['manpage/kosmorro.7'])
("man/man1", ["manpage/kosmorro.1"]),
("man/man7", ["manpage/kosmorro.7"]),
],
install_requires=[
"kosmorrolib",
"tabulate",
"termcolor",
"python-dateutil",
],
install_requires=['skyfield>=1.21.0,<2.0.0', 'tabulate', 'numpy>=1.17.0,<2.0.0', 'termcolor', 'python-dateutil'],
classifiers=[
'Development Status :: 3 - Alpha',
'Operating System :: POSIX :: Linux',
'Operating System :: MacOS :: MacOS X',
'Environment :: Console',
'Topic :: Scientific/Engineering :: Astronomy'
"Development Status :: 3 - Alpha",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Environment :: Console",
"Topic :: Scientific/Engineering :: Astronomy",
],
python_requires='>=3.7'
python_requires=">=3.7",
)

+ 0
- 7
test/__init__.py View File

@@ -1,7 +0,0 @@
from .core import *
from .data import *
from .dumper import *
from .ephemerides import *
from .events import *
from .testutils import *
from .dateutil import *

+ 0
- 39
test/core.py View File

@@ -1,39 +0,0 @@
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):
self.assertEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], core.flatten_list([0, 1, 2, [3, 4, [5, 6], 7], 8, [9]]))

def test_get_env(self):
self.assertEqual(0, len(core.get_env()))

os.environ['SOME_RANDOM_VAR'] = 'an awesome value'
self.assertEqual(0, len(core.get_env()))

os.environ['KOSMORRO_GREAT_VARIABLE'] = 'value'
env = core.get_env()
self.assertEqual(1, len(env))
self.assertEqual('value', env.great_variable)

os.environ['KOSMORRO_ANOTHER_VARIABLE'] = 'another value'
env = core.get_env()
self.assertEqual(2, len(env))
self.assertEqual('value', env.great_variable)
self.assertEqual('another value', env.another_variable)

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()

+ 0
- 17
test/data.py View File

@@ -1,17 +0,0 @@
import unittest

from kosmorrolib import data, core


class DataTestCase(unittest.TestCase):
def test_object_radius_must_be_set_to_get_apparent_radius(self):
o = data.Planet('Saturn', 'SATURN')

with self.assertRaises(ValueError) as context:
o.get_apparent_radius(core.get_timescale().now(), core.get_skf_objects()['earth'])

self.assertEqual(('Missing radius for Saturn object',), context.exception.args)


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

+ 0
- 24
test/dateutil.py View File

@@ -1,24 +0,0 @@
import unittest

from kosmorrolib import dateutil
from datetime import datetime


class DateUtilTestCase(unittest.TestCase):
def test_translate_to_timezone(self):
date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 4), to_tz=-2)
self.assertEqual(2, date.hour)

date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 0), to_tz=2)
self.assertEqual(2, date.hour)

date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 8), to_tz=2, from_tz=6)
self.assertEqual(4, date.hour)

date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 1), to_tz=0, from_tz=2)
self.assertEqual(8, date.day)
self.assertEqual(23, date.hour)


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

+ 0
- 301
test/dumper.py View File

@@ -1,301 +0,0 @@
import unittest
from datetime import date, datetime

from kosmorrolib.data import AsterEphemerides, Planet, MoonPhase, Event
from kosmorrolib.dumper import JsonDumper, TextDumper, _LatexDumper
from kosmorrolib.enum import MoonPhaseType, EventType


class DumperTestCase(unittest.TestCase):
def setUp(self) -> None:
self.maxDiff = None

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'
' "phase": "FULL_MOON",\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'
' "objects": [\n'
' {\n'
' "name": "Mars",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n'
' "event": "OPPOSITION",\n'
' "starts_at": "2019-10-14T23:00:00",\n'
' "ends_at": null,\n'
' "details": null\n'
' },\n'
' {\n'
' "objects": [\n'
' {\n'
' "name": "Venus",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n'
' "event": "MAXIMAL_ELONGATION",\n'
' "starts_at": "2019-10-14T12:00:00",\n'
' "ends_at": null,\n'
' "details": "42.0\\u00b0"\n'
' }\n'
' ]\n'
'}', JsonDumper(self._get_ephemerides(), self._get_moon_phase(), self._get_events()).to_string())

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'
' "phase": "FULL_MOON",\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'
' "objects": [\n'
' {\n'
' "name": "Mars",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n'
' "event": "OPPOSITION",\n'
' "starts_at": "2019-10-14T23:00:00",\n'
' "ends_at": null,\n'
' "details": null\n'
' },\n'
' {\n'
' "objects": [\n'
' {\n'
' "name": "Venus",\n'
' "type": "planet",\n'
' "radius": null\n'
' }\n'
' ],\n'
' "event": "MAXIMAL_ELONGATION",\n'
' "starts_at": "2019-10-14T12:00:00",\n'
' "ends_at": null,\n'
' "details": "42.0\\u00b0"\n'
' }\n'
' ]\n'
'}', 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_ephemerides()
self.assertEqual('Monday October 14, 2019\n\n'
'Object Rise time Culmination time Set time\n'
'-------- ----------- ------------------ ----------\n'
'Mars - - -\n\n'
'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, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string())

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'
'Mars 08:00 13:00 23:00\n\n'
'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, self._get_moon_phase(), [], date=date(2019, 10, 14), with_colors=False).to_string())

def test_text_dumper_with_events(self):
ephemerides = self._get_ephemerides()
self.assertEqual("Monday October 14, 2019\n\n"
"Object Rise time Culmination time Set time\n"
"-------- ----------- ------------------ ----------\n"
"Mars - - -\n\n"
"Moon phase: Full Moon\n"
"Last Quarter on Monday October 21, 2019 at 00:00\n\n"
"Expected events:\n"
"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_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):
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'
'Expected events:\n'
'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(None, self._get_moon_phase(), self._get_events(),
date=date(2019, 10, 14), with_colors=False).to_string())

def test_latex_dumper(self):
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}')
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')
self.assertRegex(latex, r'\\object\{Mars\}\{-\}\{-\}\{-\}')
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_without_ephemerides(self):
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}')
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.assertNotRegex(latex, r'\\object\{Mars\}\{-\}\{-\}\{-\}')
self.assertNotRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')

def test_latex_dumper_without_events(self):
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\}\{-\}\{-\}\{-\}')
self.assertRegex(latex, r'\\section{\\sffamily Ephemerides of the day}')

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\}')

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

@staticmethod
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 [AsterEphemerides(rise_time, culmination_time, set_time, Planet('Mars', 'MARS'))]

@staticmethod
def _get_moon_phase():
return MoonPhase(MoonPhaseType.FULL_MOON, datetime(2019, 10, 14), datetime(2019, 10, 21))

@staticmethod
def _get_events():
return [Event(EventType.OPPOSITION,
[Planet('Mars', 'MARS')],
datetime(2019, 10, 14, 23, 00)),
Event(EventType.MAXIMAL_ELONGATION,
[Planet('Venus', 'VENUS')],
datetime(2019, 10, 14, 12, 00), details='42.0°'),
]


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

+ 0
- 124
test/ephemerides.py View File

@@ -1,124 +0,0 @@
import unittest

from .testutils import expect_assertions
from kosmorrolib import ephemerides
from kosmorrolib.data import EARTH, Position, MoonPhase
from kosmorrolib.enum import MoonPhaseType

from datetime import date
from kosmorrolib.exceptions import OutOfRangeDateError


class EphemeridesTestCase(unittest.TestCase):
def test_get_ephemerides_for_aster_returns_correct_hours(self):
position = Position(0, 0, EARTH)
eph = ephemerides.get_ephemerides(date=date(2019, 11, 18),
position=position)

@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 = ephemerides.get_moon_phase(date(2019, 11, 25))
self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.isoformat(), '^2019-11-26T')

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

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

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

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

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

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

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

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

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

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

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

def test_moon_phase_prediction(self):
phase = MoonPhase(MoonPhaseType.NEW_MOON, None, None)
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.get_next_phase())
phase = MoonPhase(MoonPhaseType.WAXING_CRESCENT, None, None)
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.get_next_phase())

phase = MoonPhase(MoonPhaseType.FIRST_QUARTER, None, None)
self.assertEqual(MoonPhaseType.FULL_MOON, phase.get_next_phase())
phase = MoonPhase(MoonPhaseType.WAXING_GIBBOUS, None, None)
self.assertEqual(MoonPhaseType.FULL_MOON, phase.get_next_phase())

phase = MoonPhase(MoonPhaseType.FULL_MOON, None, None)
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.get_next_phase())
phase = MoonPhase(MoonPhaseType.WANING_GIBBOUS, None, None)
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.get_next_phase())

phase = MoonPhase(MoonPhaseType.LAST_QUARTER, None, None)
self.assertEqual(MoonPhaseType.NEW_MOON, phase.get_next_phase())
phase = MoonPhase(MoonPhaseType.WANING_CRESCENT, None, None)
self.assertEqual(MoonPhaseType.NEW_MOON, phase.get_next_phase())

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

def test_get_moon_phase_raises_exception_on_out_of_date_range(self):
with self.assertRaises(OutOfRangeDateError):
ephemerides.get_moon_phase(date(1789, 5, 5))


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

+ 0
- 83
test/events.py View File

@@ -1,83 +0,0 @@
import unittest

from datetime import date, datetime
from parameterized import parameterized

from kosmorrolib import events
from kosmorrolib.data import Event, ASTERS
from kosmorrolib.enum import EventType
from kosmorrolib.exceptions import OutOfRangeDateError

EXPECTED_EVENTS = [
(date(2020, 2, 7), []),

(date(2020, 10, 13), [Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2020, 10, 13, 23, 25))]),

(date(2022, 12, 8),
[Event(EventType.CONJUNCTION, [ASTERS[1], ASTERS[4]], datetime(2022, 12, 8, 4, 18)),
Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2022, 12, 8, 5, 41))]),

(date(2025, 1, 16), [Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2025, 1, 16, 2, 38))]),

(date(2027, 2, 19), [Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2027, 2, 19, 7, 38)),
Event(EventType.OPPOSITION, [ASTERS[4]], datetime(2027, 2, 19, 15, 50))]),

(date(2020, 1, 2), [Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 1, 2, 1, 32)),
Event(EventType.CONJUNCTION, [ASTERS[2], ASTERS[5]],
datetime(2020, 1, 2, 16, 41))]),

(date(2020, 1, 12),
[Event(EventType.CONJUNCTION, [ASTERS[2], ASTERS[6]], datetime(2020, 1, 12, 9, 51)),
Event(EventType.CONJUNCTION, [ASTERS[2], ASTERS[9]], datetime(2020, 1, 12, 10, 13)),
Event(EventType.CONJUNCTION, [ASTERS[6], ASTERS[9]], datetime(2020, 1, 12, 16, 57))]),

(date(2020, 2, 10),
[Event(EventType.MAXIMAL_ELONGATION, [ASTERS[2]], datetime(2020, 2, 10, 13, 46), details='18.2°'),
Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2020, 2, 10, 20, 34))]),

(date(2020, 3, 24),
[Event(EventType.MAXIMAL_ELONGATION, [ASTERS[2]], datetime(2020, 3, 24, 1, 56), details='27.8°'),
Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 3, 24, 15, 39)),
Event(EventType.MAXIMAL_ELONGATION, [ASTERS[3]], datetime(2020, 3, 24, 21, 58),
details='46.1°')]),

(date(2005, 6, 16),
[Event(EventType.OCCULTATION, [ASTERS[1], ASTERS[5]], datetime(2005, 6, 16, 6, 31))]),

(date(2020, 4, 7), [Event(EventType.MOON_PERIGEE, [ASTERS[1]], datetime(2020, 4, 7, 18, 14))]),

(date(2020, 1, 29), [Event(EventType.MOON_APOGEE, [ASTERS[1]], datetime(2020, 1, 29, 21, 32))])
]


class EventTestCase(unittest.TestCase):
def setUp(self) -> None:
self.maxDiff = None

@parameterized.expand(EXPECTED_EVENTS)
def test_search_events(self, d: date, expected_events: [Event]):
actual_events = events.search_events(d)
self.assertEqual(len(expected_events), len(actual_events),
'Expected %d elements, got %d for date %s.\n%s' % (len(expected_events),
len(actual_events),
d.isoformat(),
actual_events))

for i, expected_event in enumerate(expected_events):
actual_event = actual_events[i]
# Remove unnecessary precision (seconds and microseconds)
actual_event.start_time = datetime(actual_event.start_time.year,
actual_event.start_time.month,
actual_event.start_time.day,
actual_event.start_time.hour,
actual_event.start_time.minute)

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

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


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

+ 0
- 48
test/testutils.py View File

@@ -1,48 +0,0 @@
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

Loading…
Cancel
Save