From 87206d706893fef714e955cf6047483bc815a327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Deuchnord?= Date: Sat, 8 May 2021 17:59:20 +0200 Subject: [PATCH] feat: use Kosmorrolib (#162) --- .github/workflows/black.yml | 19 + .github/workflows/codeql-analysis.yml | 66 --- .github/workflows/i18n.yml | 2 +- .github/workflows/pylint.yml | 26 - .github/workflows/unit-tests.yml | 42 -- .scripts/tests-e2e.sh | 50 +- MANIFEST.in | 4 +- Makefile | 17 +- Pipfile | 11 +- Pipfile.lock | 373 +++++-------- {kosmorrolib => _kosmorro}/__init__.py | 0 .../version.py => _kosmorro/__version__.py | 9 +- .../assets/moonphases/png/first-quarter.png | Bin .../assets/moonphases/png/full-moon.png | Bin .../assets/moonphases/png/last-quarter.png | Bin .../assets/moonphases/png/new-moon.png | Bin .../assets/moonphases/png/unknown.png | Bin .../assets/moonphases/png/waning-crescent.png | Bin .../assets/moonphases/png/waning-gibbous.png | Bin .../assets/moonphases/png/waxing-crescent.png | Bin .../assets/moonphases/png/waxing-gibbous.png | Bin .../assets/moonphases/svg/first-quarter.svg | 0 .../assets/moonphases/svg/full-moon.svg | 0 .../assets/moonphases/svg/last-quarter.svg | 0 .../assets/moonphases/svg/new-moon.svg | 0 .../assets/moonphases/svg/unknown.svg | 0 .../assets/moonphases/svg/waning-crescent.svg | 0 .../assets/moonphases/svg/waning-gibbous.svg | 0 .../assets/moonphases/svg/waxing-crescent.svg | 0 .../assets/moonphases/svg/waxing-gibbous.svg | 0 .../assets/pdf/kosmorro.sty | 0 .../assets/pdf/template.tex | 0 .../assets/png/kosmorro-icon-white.png | Bin .../assets/png/kosmorro-icon.png | Bin .../assets/png/kosmorro-logo-white.png | Bin .../assets/png/kosmorro-logo.png | Bin .../assets/svg/kosmorro-icon-white.svg | 0 .../assets/svg/kosmorro-icon.svg | 0 .../assets/svg/kosmorro-logo-white.svg | 0 .../assets/svg/kosmorro-logo.svg | 0 _kosmorro/date.py | 42 ++ _kosmorro/debug.py | 15 + _kosmorro/dumper.py | 527 ++++++++++++++++++ .../enum.py => _kosmorro/environment.py | 57 +- {kosmorrolib => _kosmorro}/exceptions.py | 11 +- _kosmorro/i18n/__init__.py | 0 _kosmorro/i18n/strings.py | 56 ++ .../i18n.py => _kosmorro/i18n/utils.py | 20 +- .../locales/de/LC_MESSAGES/messages.po | 0 .../locales/es/LC_MESSAGES/messages.po | 0 .../locales/fr/LC_MESSAGES/messages.po | 0 .../locales/messages.pot | 336 +++++------ .../locales/nb_NO/LC_MESSAGES/messages.po | 0 .../locales/nl/LC_MESSAGES/messages.po | 0 .../locales/ru/LC_MESSAGES/messages.po | 0 _kosmorro/main.py | 330 +++++++++++ kosmorro | 6 +- kosmorrolib/core.py | 119 ---- kosmorrolib/data.py | 224 -------- kosmorrolib/dateutil.py | 28 - kosmorrolib/dumper.py | 385 ------------- kosmorrolib/ephemerides.py | 163 ------ kosmorrolib/events.py | 199 ------- kosmorrolib/main.py | 195 ------- setup.py | 47 +- test/__init__.py | 7 - test/core.py | 39 -- test/data.py | 17 - test/dateutil.py | 24 - test/dumper.py | 301 ---------- test/ephemerides.py | 124 ----- test/events.py | 83 --- test/testutils.py | 48 -- 73 files changed, 1438 insertions(+), 2584 deletions(-) create mode 100644 .github/workflows/black.yml delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/pylint.yml rename {kosmorrolib => _kosmorro}/__init__.py (100%) rename kosmorrolib/version.py => _kosmorro/__version__.py (73%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/first-quarter.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/full-moon.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/last-quarter.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/new-moon.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/unknown.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/waning-crescent.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/waning-gibbous.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/waxing-crescent.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/png/waxing-gibbous.png (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/first-quarter.svg (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/full-moon.svg (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/last-quarter.svg (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/new-moon.svg (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/unknown.svg (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/waning-crescent.svg (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/waning-gibbous.svg (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/waxing-crescent.svg (100%) rename {kosmorrolib => _kosmorro}/assets/moonphases/svg/waxing-gibbous.svg (100%) rename {kosmorrolib => _kosmorro}/assets/pdf/kosmorro.sty (100%) rename {kosmorrolib => _kosmorro}/assets/pdf/template.tex (100%) rename {kosmorrolib => _kosmorro}/assets/png/kosmorro-icon-white.png (100%) rename {kosmorrolib => _kosmorro}/assets/png/kosmorro-icon.png (100%) rename {kosmorrolib => _kosmorro}/assets/png/kosmorro-logo-white.png (100%) rename {kosmorrolib => _kosmorro}/assets/png/kosmorro-logo.png (100%) rename {kosmorrolib => _kosmorro}/assets/svg/kosmorro-icon-white.svg (100%) rename {kosmorrolib => _kosmorro}/assets/svg/kosmorro-icon.svg (100%) rename {kosmorrolib => _kosmorro}/assets/svg/kosmorro-logo-white.svg (100%) rename {kosmorrolib => _kosmorro}/assets/svg/kosmorro-logo.svg (100%) create mode 100644 _kosmorro/date.py create mode 100644 _kosmorro/debug.py create mode 100644 _kosmorro/dumper.py rename kosmorrolib/enum.py => _kosmorro/environment.py (53%) rename {kosmorrolib => _kosmorro}/exceptions.py (80%) create mode 100644 _kosmorro/i18n/__init__.py create mode 100644 _kosmorro/i18n/strings.py rename kosmorrolib/i18n.py => _kosmorro/i18n/utils.py (63%) rename {kosmorrolib => _kosmorro}/locales/de/LC_MESSAGES/messages.po (100%) rename {kosmorrolib => _kosmorro}/locales/es/LC_MESSAGES/messages.po (100%) rename {kosmorrolib => _kosmorro}/locales/fr/LC_MESSAGES/messages.po (100%) rename {kosmorrolib => _kosmorro}/locales/messages.pot (66%) rename {kosmorrolib => _kosmorro}/locales/nb_NO/LC_MESSAGES/messages.po (100%) rename {kosmorrolib => _kosmorro}/locales/nl/LC_MESSAGES/messages.po (100%) rename {kosmorrolib => _kosmorro}/locales/ru/LC_MESSAGES/messages.po (100%) create mode 100644 _kosmorro/main.py delete mode 100644 kosmorrolib/core.py delete mode 100644 kosmorrolib/data.py delete mode 100644 kosmorrolib/dateutil.py delete mode 100644 kosmorrolib/dumper.py delete mode 100644 kosmorrolib/ephemerides.py delete mode 100644 kosmorrolib/events.py delete mode 100644 kosmorrolib/main.py delete mode 100644 test/__init__.py delete mode 100644 test/core.py delete mode 100644 test/data.py delete mode 100644 test/dateutil.py delete mode 100644 test/dumper.py delete mode 100644 test/ephemerides.py delete mode 100644 test/testutils.py diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 0000000..2ddee4a --- /dev/null +++ b/.github/workflows/black.yml @@ -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 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 6c147c1..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml index 2ddd167..95d77e9 100644 --- a/.github/workflows/i18n.yml +++ b/.github/workflows/i18n.yml @@ -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 diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml deleted file mode 100644 index 7151b2b..0000000 --- a/.github/workflows/pylint.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index a9411f0..e69de29 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -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 diff --git a/.scripts/tests-e2e.sh b/.scripts/tests-e2e.sh index 35363ac..6b823ff 100755 --- a/.scripts/tests-e2e.sh +++ b/.scripts/tests-e2e.sh @@ -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" diff --git a/MANIFEST.in b/MANIFEST.in index 462d2e1..52a10d1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -recursive-include kosmorrolib/locales * -recursive-include kosmorrolib/assets * +recursive-include _kosmorro/locales * +recursive-include _kosmorro/assets * diff --git a/Makefile b/Makefile index b69c07d..55fd014 100644 --- a/Makefile +++ b/Makefile @@ -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} diff --git a/Pipfile b/Pipfile index 0e3da88..6d9d4d7 100644 --- a/Pipfile +++ b/Pipfile @@ -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 diff --git a/Pipfile.lock b/Pipfile.lock index 405dd0c..7efd5eb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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" } } } diff --git a/kosmorrolib/__init__.py b/_kosmorro/__init__.py similarity index 100% rename from kosmorrolib/__init__.py rename to _kosmorro/__init__.py diff --git a/kosmorrolib/version.py b/_kosmorro/__version__.py similarity index 73% rename from kosmorrolib/version.py rename to _kosmorro/__version__.py index 0d09c55..6afb826 100644 --- a/kosmorrolib/version.py +++ b/_kosmorro/__version__.py @@ -16,4 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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" diff --git a/kosmorrolib/assets/moonphases/png/first-quarter.png b/_kosmorro/assets/moonphases/png/first-quarter.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/first-quarter.png rename to _kosmorro/assets/moonphases/png/first-quarter.png diff --git a/kosmorrolib/assets/moonphases/png/full-moon.png b/_kosmorro/assets/moonphases/png/full-moon.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/full-moon.png rename to _kosmorro/assets/moonphases/png/full-moon.png diff --git a/kosmorrolib/assets/moonphases/png/last-quarter.png b/_kosmorro/assets/moonphases/png/last-quarter.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/last-quarter.png rename to _kosmorro/assets/moonphases/png/last-quarter.png diff --git a/kosmorrolib/assets/moonphases/png/new-moon.png b/_kosmorro/assets/moonphases/png/new-moon.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/new-moon.png rename to _kosmorro/assets/moonphases/png/new-moon.png diff --git a/kosmorrolib/assets/moonphases/png/unknown.png b/_kosmorro/assets/moonphases/png/unknown.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/unknown.png rename to _kosmorro/assets/moonphases/png/unknown.png diff --git a/kosmorrolib/assets/moonphases/png/waning-crescent.png b/_kosmorro/assets/moonphases/png/waning-crescent.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/waning-crescent.png rename to _kosmorro/assets/moonphases/png/waning-crescent.png diff --git a/kosmorrolib/assets/moonphases/png/waning-gibbous.png b/_kosmorro/assets/moonphases/png/waning-gibbous.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/waning-gibbous.png rename to _kosmorro/assets/moonphases/png/waning-gibbous.png diff --git a/kosmorrolib/assets/moonphases/png/waxing-crescent.png b/_kosmorro/assets/moonphases/png/waxing-crescent.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/waxing-crescent.png rename to _kosmorro/assets/moonphases/png/waxing-crescent.png diff --git a/kosmorrolib/assets/moonphases/png/waxing-gibbous.png b/_kosmorro/assets/moonphases/png/waxing-gibbous.png similarity index 100% rename from kosmorrolib/assets/moonphases/png/waxing-gibbous.png rename to _kosmorro/assets/moonphases/png/waxing-gibbous.png diff --git a/kosmorrolib/assets/moonphases/svg/first-quarter.svg b/_kosmorro/assets/moonphases/svg/first-quarter.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/first-quarter.svg rename to _kosmorro/assets/moonphases/svg/first-quarter.svg diff --git a/kosmorrolib/assets/moonphases/svg/full-moon.svg b/_kosmorro/assets/moonphases/svg/full-moon.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/full-moon.svg rename to _kosmorro/assets/moonphases/svg/full-moon.svg diff --git a/kosmorrolib/assets/moonphases/svg/last-quarter.svg b/_kosmorro/assets/moonphases/svg/last-quarter.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/last-quarter.svg rename to _kosmorro/assets/moonphases/svg/last-quarter.svg diff --git a/kosmorrolib/assets/moonphases/svg/new-moon.svg b/_kosmorro/assets/moonphases/svg/new-moon.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/new-moon.svg rename to _kosmorro/assets/moonphases/svg/new-moon.svg diff --git a/kosmorrolib/assets/moonphases/svg/unknown.svg b/_kosmorro/assets/moonphases/svg/unknown.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/unknown.svg rename to _kosmorro/assets/moonphases/svg/unknown.svg diff --git a/kosmorrolib/assets/moonphases/svg/waning-crescent.svg b/_kosmorro/assets/moonphases/svg/waning-crescent.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/waning-crescent.svg rename to _kosmorro/assets/moonphases/svg/waning-crescent.svg diff --git a/kosmorrolib/assets/moonphases/svg/waning-gibbous.svg b/_kosmorro/assets/moonphases/svg/waning-gibbous.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/waning-gibbous.svg rename to _kosmorro/assets/moonphases/svg/waning-gibbous.svg diff --git a/kosmorrolib/assets/moonphases/svg/waxing-crescent.svg b/_kosmorro/assets/moonphases/svg/waxing-crescent.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/waxing-crescent.svg rename to _kosmorro/assets/moonphases/svg/waxing-crescent.svg diff --git a/kosmorrolib/assets/moonphases/svg/waxing-gibbous.svg b/_kosmorro/assets/moonphases/svg/waxing-gibbous.svg similarity index 100% rename from kosmorrolib/assets/moonphases/svg/waxing-gibbous.svg rename to _kosmorro/assets/moonphases/svg/waxing-gibbous.svg diff --git a/kosmorrolib/assets/pdf/kosmorro.sty b/_kosmorro/assets/pdf/kosmorro.sty similarity index 100% rename from kosmorrolib/assets/pdf/kosmorro.sty rename to _kosmorro/assets/pdf/kosmorro.sty diff --git a/kosmorrolib/assets/pdf/template.tex b/_kosmorro/assets/pdf/template.tex similarity index 100% rename from kosmorrolib/assets/pdf/template.tex rename to _kosmorro/assets/pdf/template.tex diff --git a/kosmorrolib/assets/png/kosmorro-icon-white.png b/_kosmorro/assets/png/kosmorro-icon-white.png similarity index 100% rename from kosmorrolib/assets/png/kosmorro-icon-white.png rename to _kosmorro/assets/png/kosmorro-icon-white.png diff --git a/kosmorrolib/assets/png/kosmorro-icon.png b/_kosmorro/assets/png/kosmorro-icon.png similarity index 100% rename from kosmorrolib/assets/png/kosmorro-icon.png rename to _kosmorro/assets/png/kosmorro-icon.png diff --git a/kosmorrolib/assets/png/kosmorro-logo-white.png b/_kosmorro/assets/png/kosmorro-logo-white.png similarity index 100% rename from kosmorrolib/assets/png/kosmorro-logo-white.png rename to _kosmorro/assets/png/kosmorro-logo-white.png diff --git a/kosmorrolib/assets/png/kosmorro-logo.png b/_kosmorro/assets/png/kosmorro-logo.png similarity index 100% rename from kosmorrolib/assets/png/kosmorro-logo.png rename to _kosmorro/assets/png/kosmorro-logo.png diff --git a/kosmorrolib/assets/svg/kosmorro-icon-white.svg b/_kosmorro/assets/svg/kosmorro-icon-white.svg similarity index 100% rename from kosmorrolib/assets/svg/kosmorro-icon-white.svg rename to _kosmorro/assets/svg/kosmorro-icon-white.svg diff --git a/kosmorrolib/assets/svg/kosmorro-icon.svg b/_kosmorro/assets/svg/kosmorro-icon.svg similarity index 100% rename from kosmorrolib/assets/svg/kosmorro-icon.svg rename to _kosmorro/assets/svg/kosmorro-icon.svg diff --git a/kosmorrolib/assets/svg/kosmorro-logo-white.svg b/_kosmorro/assets/svg/kosmorro-logo-white.svg similarity index 100% rename from kosmorrolib/assets/svg/kosmorro-logo-white.svg rename to _kosmorro/assets/svg/kosmorro-logo-white.svg diff --git a/kosmorrolib/assets/svg/kosmorro-logo.svg b/_kosmorro/assets/svg/kosmorro-logo.svg similarity index 100% rename from kosmorrolib/assets/svg/kosmorro-logo.svg rename to _kosmorro/assets/svg/kosmorro-logo.svg diff --git a/_kosmorro/date.py b/_kosmorro/date.py new file mode 100644 index 0000000..5d25a71 --- /dev/null +++ b/_kosmorro/date.py @@ -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)) diff --git a/_kosmorro/debug.py b/_kosmorro/debug.py new file mode 100644 index 0000000..72b04c1 --- /dev/null +++ b/_kosmorro/debug.py @@ -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) diff --git a/_kosmorro/dumper.py b/_kosmorro/dumper.py new file mode 100644 index 0000000..50d5c0e --- /dev/null +++ b/_kosmorro/dumper.py @@ -0,0 +1,527 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# 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 . + +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()) diff --git a/kosmorrolib/enum.py b/_kosmorro/environment.py similarity index 53% rename from kosmorrolib/enum.py rename to _kosmorro/environment.py index 718e8cf..18542c3 100644 --- a/kosmorrolib/enum.py +++ b/_kosmorro/environment.py @@ -16,25 +16,38 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/kosmorrolib/exceptions.py b/_kosmorro/exceptions.py similarity index 80% rename from kosmorrolib/exceptions.py rename to _kosmorro/exceptions.py index de64af2..46cc3e2 100644 --- a/kosmorrolib/exceptions.py +++ b/_kosmorro/exceptions.py @@ -17,7 +17,7 @@ # along with this program. If not, see . 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): diff --git a/_kosmorro/i18n/__init__.py b/_kosmorro/i18n/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/_kosmorro/i18n/strings.py b/_kosmorro/i18n/strings.py new file mode 100644 index 0000000..798dbf0 --- /dev/null +++ b/_kosmorro/i18n/strings.py @@ -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")) diff --git a/kosmorrolib/i18n.py b/_kosmorro/i18n/utils.py similarity index 63% rename from kosmorrolib/i18n.py rename to _kosmorro/i18n/utils.py index d0257b1..b65d525 100644 --- a/kosmorrolib/i18n.py +++ b/_kosmorro/i18n/utils.py @@ -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): diff --git a/kosmorrolib/locales/de/LC_MESSAGES/messages.po b/_kosmorro/locales/de/LC_MESSAGES/messages.po similarity index 100% rename from kosmorrolib/locales/de/LC_MESSAGES/messages.po rename to _kosmorro/locales/de/LC_MESSAGES/messages.po diff --git a/kosmorrolib/locales/es/LC_MESSAGES/messages.po b/_kosmorro/locales/es/LC_MESSAGES/messages.po similarity index 100% rename from kosmorrolib/locales/es/LC_MESSAGES/messages.po rename to _kosmorro/locales/es/LC_MESSAGES/messages.po diff --git a/kosmorrolib/locales/fr/LC_MESSAGES/messages.po b/_kosmorro/locales/fr/LC_MESSAGES/messages.po similarity index 100% rename from kosmorrolib/locales/fr/LC_MESSAGES/messages.po rename to _kosmorro/locales/fr/LC_MESSAGES/messages.po diff --git a/kosmorrolib/locales/messages.pot b/_kosmorro/locales/messages.pot similarity index 66% rename from kosmorrolib/locales/messages.pot rename to _kosmorro/locales/messages.pot index 5a7136c..79902c3 100644 --- a/kosmorrolib/locales/messages.pot +++ b/_kosmorro/locales/messages.pot @@ -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 \n" "Language-Team: LANGUAGE \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 "" diff --git a/kosmorrolib/locales/nb_NO/LC_MESSAGES/messages.po b/_kosmorro/locales/nb_NO/LC_MESSAGES/messages.po similarity index 100% rename from kosmorrolib/locales/nb_NO/LC_MESSAGES/messages.po rename to _kosmorro/locales/nb_NO/LC_MESSAGES/messages.po diff --git a/kosmorrolib/locales/nl/LC_MESSAGES/messages.po b/_kosmorro/locales/nl/LC_MESSAGES/messages.po similarity index 100% rename from kosmorrolib/locales/nl/LC_MESSAGES/messages.po rename to _kosmorro/locales/nl/LC_MESSAGES/messages.po diff --git a/kosmorrolib/locales/ru/LC_MESSAGES/messages.po b/_kosmorro/locales/ru/LC_MESSAGES/messages.po similarity index 100% rename from kosmorrolib/locales/ru/LC_MESSAGES/messages.po rename to _kosmorro/locales/ru/LC_MESSAGES/messages.po diff --git a/_kosmorro/main.py b/_kosmorro/main.py new file mode 100644 index 0000000..e60ebf0 --- /dev/null +++ b/_kosmorro/main.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 + +# Kosmorro - Compute The Next Ephemerides +# Copyright (C) 2019 Jérôme Deuchnord +# +# 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 . + +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() diff --git a/kosmorro b/kosmorro index f0b6e77..7b10fe5 100755 --- a/kosmorro +++ b/kosmorro @@ -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: diff --git a/kosmorrolib/core.py b/kosmorrolib/core.py deleted file mode 100644 index 36989b5..0000000 --- a/kosmorrolib/core.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 - -# Kosmorro - Compute The Next Ephemerides -# Copyright (C) 2019 Jérôme Deuchnord -# -# 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 . - -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)) diff --git a/kosmorrolib/data.py b/kosmorrolib/data.py deleted file mode 100644 index ea30eb4..0000000 --- a/kosmorrolib/data.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 - -# Kosmorro - Compute The Next Ephemerides -# Copyright (C) 2019 Jérôme Deuchnord -# -# 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 . - -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 '' % (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 '' % (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 diff --git a/kosmorrolib/dateutil.py b/kosmorrolib/dateutil.py deleted file mode 100644 index 6ff64d4..0000000 --- a/kosmorrolib/dateutil.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 - -# Kosmorro - Compute The Next Ephemerides -# Copyright (C) 2019 Jérôme Deuchnord -# -# 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 . - -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))) diff --git a/kosmorrolib/dumper.py b/kosmorrolib/dumper.py deleted file mode 100644 index e12d558..0000000 --- a/kosmorrolib/dumper.py +++ /dev/null @@ -1,385 +0,0 @@ -#!/usr/bin/env python3 - -# Kosmorro - Compute The Next Ephemerides -# Copyright (C) 2019 Jérôme Deuchnord -# -# 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 . - -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()) diff --git a/kosmorrolib/ephemerides.py b/kosmorrolib/ephemerides.py deleted file mode 100644 index 2ddc578..0000000 --- a/kosmorrolib/ephemerides.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 - -# Kosmorro - Compute The Next Ephemerides -# Copyright (C) 2019 Jérôme Deuchnord -# -# 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 . - -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 diff --git a/kosmorrolib/events.py b/kosmorrolib/events.py deleted file mode 100644 index 661246f..0000000 --- a/kosmorrolib/events.py +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env python3 - -# Kosmorro - Compute The Next Ephemerides -# Copyright (C) 2019 Jérôme Deuchnord -# -# 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 . - -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 diff --git a/kosmorrolib/main.py b/kosmorrolib/main.py deleted file mode 100644 index c971ffe..0000000 --- a/kosmorrolib/main.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python3 - -# Kosmorro - Compute The Next Ephemerides -# Copyright (C) 2019 Jérôme Deuchnord -# -# 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 . - -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() diff --git a/setup.py b/setup.py index badbe2f..ee01c8f 100644 --- a/setup.py +++ b/setup.py @@ -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", ) diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index bdb6885..0000000 --- a/test/__init__.py +++ /dev/null @@ -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 * diff --git a/test/core.py b/test/core.py deleted file mode 100644 index 34cf8e5..0000000 --- a/test/core.py +++ /dev/null @@ -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() diff --git a/test/data.py b/test/data.py deleted file mode 100644 index d1e2103..0000000 --- a/test/data.py +++ /dev/null @@ -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() diff --git a/test/dateutil.py b/test/dateutil.py deleted file mode 100644 index 984add3..0000000 --- a/test/dateutil.py +++ /dev/null @@ -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() diff --git a/test/dumper.py b/test/dumper.py deleted file mode 100644 index e37b572..0000000 --- a/test/dumper.py +++ /dev/null @@ -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() diff --git a/test/ephemerides.py b/test/ephemerides.py deleted file mode 100644 index a854ef7..0000000 --- a/test/ephemerides.py +++ /dev/null @@ -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() diff --git a/test/events.py b/test/events.py index 5008489..e69de29 100644 --- a/test/events.py +++ b/test/events.py @@ -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() diff --git a/test/testutils.py b/test/testutils.py deleted file mode 100644 index a1828eb..0000000 --- a/test/testutils.py +++ /dev/null @@ -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