From dd8a885e81030b8b87659cea2ddac8766fd3d0d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kier=C3=A1n=20Meinhardt?= Date: Tue, 14 Dec 2021 23:23:23 +0100 Subject: [PATCH] feat: add transits script --- packages/scripts/horoscope/poetry.lock | 70 ++++++++- packages/scripts/horoscope/pyproject.toml | 4 + packages/scripts/horoscope/transits.py | 181 ++++++++++++++++++++++ 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 packages/scripts/horoscope/transits.py diff --git a/packages/scripts/horoscope/poetry.lock b/packages/scripts/horoscope/poetry.lock index 91d42be..84614f3 100644 --- a/packages/scripts/horoscope/poetry.lock +++ b/packages/scripts/horoscope/poetry.lock @@ -28,6 +28,14 @@ python-versions = "*" [package.dependencies] pyswisseph = "2.08.00-1" +[[package]] +name = "numpy" +version = "1.21.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "pyswisseph" version = "2.08.00-1" @@ -36,10 +44,32 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pytz" +version = "2021.3" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "timezonefinder" +version = "5.2.0" +description = "fast python package for finding the timezone of any point on earth (coordinates) offline" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +numpy = ">=1.16" + +[package.extras] +numba = ["numba (>=0.48)"] + [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "2f40aecf583ff9e4f7b2dcc090fee27915e64ff1f8a450fbe5e6f95e8c487d75" +content-hash = "657742383232643f2fa13df5686de0cc79c624f9ae9bdb2f0fc96c7a94b5b8bf" [metadata.files] click = [ @@ -54,6 +84,44 @@ flatlib = [ {file = "flatlib-0.2.3-py3-none-any.whl", hash = "sha256:c846d83c965db7588581bb65ac9a6668b9a190afcad5027269f7e9c75f467bcd"}, {file = "flatlib-0.2.3.tar.gz", hash = "sha256:46cc956b936aa31a96082cff23448a5c27dd6e5e434a6293bc9265336c00dd5d"}, ] +numpy = [ + {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671"}, + {file = "numpy-1.21.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e"}, + {file = "numpy-1.21.1-cp37-cp37m-win32.whl", hash = "sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172"}, + {file = "numpy-1.21.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267"}, + {file = "numpy-1.21.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68"}, + {file = "numpy-1.21.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8"}, + {file = "numpy-1.21.1-cp38-cp38-win32.whl", hash = "sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd"}, + {file = "numpy-1.21.1-cp38-cp38-win_amd64.whl", hash = "sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b"}, + {file = "numpy-1.21.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1"}, + {file = "numpy-1.21.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a"}, + {file = "numpy-1.21.1-cp39-cp39-win32.whl", hash = "sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2"}, + {file = "numpy-1.21.1-cp39-cp39-win_amd64.whl", hash = "sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33"}, + {file = "numpy-1.21.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4"}, + {file = "numpy-1.21.1.zip", hash = "sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd"}, +] pyswisseph = [ {file = "pyswisseph-2.08.00-1.tar.gz", hash = "sha256:6b4818c0224d309c0b01f3c52df2432900dddcde345364408d99eafc9cdd1e71"}, ] +pytz = [ + {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, + {file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"}, +] +timezonefinder = [ + {file = "timezonefinder-5.2.0-py36.py37.py38-none-any.whl", hash = "sha256:4545533086eb25cd7ba10b97785059acbababf4577ab1b4d5c2ab56642eadfea"}, + {file = "timezonefinder-5.2.0.tar.gz", hash = "sha256:a374570295a8dbd923630ce85f754e52578e288cb0a9cf575834415e84758352"}, +] diff --git a/packages/scripts/horoscope/pyproject.toml b/packages/scripts/horoscope/pyproject.toml index 82a8803..64ad6e4 100644 --- a/packages/scripts/horoscope/pyproject.toml +++ b/packages/scripts/horoscope/pyproject.toml @@ -8,9 +8,13 @@ authors = ["Kierán Meinhardt "] python = "^3.8" flatlib = "^0.2.3" click = "^8.0.3" +timezonefinder = "^5.2.0" +pytz = "^2021.3" [tool.poetry.scripts] horoscope = "horoscope:main" +transits-current = "transits:current" +transits-forecast = "transits:forecast" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/packages/scripts/horoscope/transits.py b/packages/scripts/horoscope/transits.py new file mode 100644 index 0000000..1e30387 --- /dev/null +++ b/packages/scripts/horoscope/transits.py @@ -0,0 +1,181 @@ +from flatlib import aspects, const +from flatlib.chart import Chart +from flatlib.datetime import Datetime +import pytz +from flatlib.geopos import GeoPos +import timezonefinder +import operator +import click +import itertools +from datetime import datetime, timedelta + +tf = timezonefinder.TimezoneFinder() + +planets = [ + const.SUN, + const.MOON, + const.MERCURY, + const.VENUS, + const.MARS, + const.JUPITER, + const.SATURN, + const.URANUS, + const.NEPTUNE, + const.PLUTO, +] + +planet_symbols = { + const.SUN: "☉", + const.MOON: "☽", + const.MERCURY: "☿", + const.VENUS: "♀", + const.MARS: "♂", + const.JUPITER: "♃", + const.SATURN: "♄", + const.URANUS: "♅", + const.NEPTUNE: "♆", + const.PLUTO: "⯓", +} + +aspect_symbols = { + const.NO_ASPECT: " ", + const.CONJUNCTION: "☌", + const.SEXTILE: "⚹", + const.SQUARE: "□", + const.TRINE: "△", + const.OPPOSITION: "☍", +} + + +def convert_into_stupid_flatlib_format(dt): + return Datetime( + dt.strftime("%Y/%m/%d"), + dt.strftime("%H:%M"), + dt.utcoffset().total_seconds() / 3600, + ) + + +here_latitude = 52.52 +here_longitude = 13.4 + + +def get_aspects(chart1, chart2, *, threshold): + for planet1 in chart1.objects: + for planet2 in chart2.objects: + aspect = aspects.getAspect(planet1, planet2, const.MAJOR_ASPECTS) + if aspect.exists() and aspect.orb <= threshold: + yield aspect + + +def get_chart(position, dt_naive): + timezone = pytz.timezone(tf.timezone_at(lat=position.lat, lng=position.lon)) + dt_aware = timezone.localize(dt_naive) + return Chart(convert_into_stupid_flatlib_format(dt_aware), position, IDs=planets) + + +def show_aspect(aspect): + return " ".join( + [ + planet_symbols[aspect.active.id], + aspect_symbols[aspect.type], + planet_symbols[aspect.passive.id], + ] + ) + + +@click.command() +@click.option("--natal-latitude", type=click.FLOAT, default=here_latitude) +@click.option("--natal-longitude", type=click.FLOAT, default=here_longitude) +@click.option("--natal-date", type=click.DateTime(), default=datetime.now()) +@click.option("--transit-latitude", type=click.FLOAT, default=here_latitude) +@click.option("--transit-longitude", type=click.FLOAT, default=here_longitude) +@click.option("--transit-date", type=click.DateTime(), default=datetime.now()) +@click.option("--threshold", type=click.FLOAT, default=5) +def forecast( + natal_latitude: float, + natal_longitude: float, + natal_date: datetime, + transit_latitude: float, + transit_longitude: float, + transit_date: datetime, + threshold: float, +): + transit_position = GeoPos(transit_latitude, transit_longitude) + natal_position = GeoPos(natal_latitude, natal_longitude) + natal_chart = get_chart(natal_position, natal_date) + transit_chart = get_chart(transit_position, transit_date) + + offset = 0 + previous_aspects = set( + show_aspect(a) + for a in get_aspects(natal_chart, transit_chart, threshold=threshold) + ) + while True: + then = transit_date + timedelta(minutes=offset) + current_chart = get_chart(transit_position, then) + current_aspects = set( + show_aspect(a) + for a in get_aspects(natal_chart, current_chart, threshold=threshold) + ) + entered = current_aspects - previous_aspects + exited = previous_aspects - current_aspects + if entered or exited: + print( + then.strftime("%Y-%m-%d %H:%M"), + "".join([" | +" + a for a in entered] + [" | -" + a for a in exited]), + sep="", + ) + previous_aspects = current_aspects + offset += 1 + + +@click.command() +@click.option("--natal-latitude", type=click.FLOAT, default=here_latitude) +@click.option("--natal-longitude", type=click.FLOAT, default=here_longitude) +@click.option("--natal-date", "-D", type=click.DateTime(), default=datetime.now()) +@click.option("--transit-latitude", type=click.FLOAT, default=here_latitude) +@click.option("--transit-longitude", type=click.FLOAT, default=here_longitude) +@click.option("--transit-date", "-d", type=click.DateTime(), default=datetime.now()) +@click.option("--threshold", "-t", type=click.FLOAT, default=5) +def current( + natal_latitude: float, + natal_longitude: float, + natal_date: datetime, + transit_latitude: float, + transit_longitude: float, + transit_date: datetime, + threshold: float, +): + transit_position = GeoPos(transit_latitude, transit_longitude) + natal_position = GeoPos(natal_latitude, natal_longitude) + natal_chart = get_chart(natal_position, natal_date) + transit_chart = get_chart(transit_position, transit_date) + + relevant_aspects = list( + get_aspects(natal_chart, transit_chart, threshold=threshold) + ) + + def aspect_switch_date(aspect, *, direction=1, threshold): + offset = 0 + while True: + then = transit_date + direction * timedelta(days=offset) + current_chart = get_chart(transit_position, then) + aspects = [ + show_aspect(a) + for a in get_aspects(natal_chart, current_chart, threshold=threshold) + ] + if aspect not in aspects: + return then.date() + offset += 1 + + for aspect in sorted(relevant_aspects, key=operator.attrgetter("orb")): + aspect_string = show_aspect(aspect) + print( + aspect_switch_date( + aspect_string, direction=-1, threshold=threshold + ).isoformat(), + aspect_switch_date( + aspect_string, direction=1, threshold=threshold + ).isoformat(), + aspect_string, + )