From 79bffac7f9eda1b3be76f2a4ff18d81b44c74390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kier=C3=A1n=20Meinhardt?= Date: Fri, 24 Feb 2023 21:51:04 +0100 Subject: [PATCH] horoscope: add --- horoscope/default.nix | 4 + horoscope/horoscope.py | 54 ++++++++++++ horoscope/poetry.lock | 81 ++++++++++++++++++ horoscope/pyproject.toml | 21 +++++ horoscope/transits.py | 181 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 341 insertions(+) create mode 100644 horoscope/default.nix create mode 100644 horoscope/horoscope.py create mode 100644 horoscope/poetry.lock create mode 100644 horoscope/pyproject.toml create mode 100644 horoscope/transits.py diff --git a/horoscope/default.nix b/horoscope/default.nix new file mode 100644 index 0000000..8e8efc5 --- /dev/null +++ b/horoscope/default.nix @@ -0,0 +1,4 @@ +{poetry2nix}: +poetry2nix.mkPoetryApplication { + projectDir = ./.; +} diff --git a/horoscope/horoscope.py b/horoscope/horoscope.py new file mode 100644 index 0000000..34cfe81 --- /dev/null +++ b/horoscope/horoscope.py @@ -0,0 +1,54 @@ +from datetime import datetime +import click +from flatlib.datetime import Datetime +from flatlib.geopos import GeoPos +from flatlib.chart import Chart +import flatlib.const + +sign_symbols = { + flatlib.const.ARIES: "♈", + flatlib.const.TAURUS: "♉", + flatlib.const.GEMINI: "♊", + flatlib.const.CANCER: "♋", + flatlib.const.LEO: "♌", + flatlib.const.VIRGO: "♍", + flatlib.const.LIBRA: "♎", + flatlib.const.SCORPIO: "♏", + flatlib.const.SAGITTARIUS: "♐", + flatlib.const.CAPRICORN: "♑", + flatlib.const.AQUARIUS: "♒", + flatlib.const.PISCES: "♓", +} + +planet_symbols = { + flatlib.const.SUN: "☉", + flatlib.const.MOON: "☽", + flatlib.const.MERCURY: "☿", + flatlib.const.VENUS: "♀", + flatlib.const.MARS: "♂", + flatlib.const.JUPITER: "♃", + flatlib.const.SATURN: "♄", +} + + +def convert_into_stupid_flatlib_format(dt): + return Datetime(dt.strftime("%Y/%m/%d"), dt.strftime("%H:%M")) + + +@click.command() +@click.option("--latitude", type=click.FLOAT, required=True) +@click.option("--longitude", type=click.FLOAT, required=True) +@click.option("--date", type=click.DateTime(), default=datetime.now()) +def main(latitude: float, longitude: float, date: datetime): + flatlib_datetime = convert_into_stupid_flatlib_format(date) + position = GeoPos(latitude, longitude) + chart = Chart(flatlib_datetime, position) + for planet in planet_symbols.keys(): + planet_position = chart.getObject(planet) + print( + planet_symbols[planet], + sign_symbols[planet_position.sign], + "℞" if planet_position.movement() == flatlib.const.RETROGRADE else "", + end="", + ) + print() diff --git a/horoscope/poetry.lock b/horoscope/poetry.lock new file mode 100644 index 0000000..633f369 --- /dev/null +++ b/horoscope/poetry.lock @@ -0,0 +1,81 @@ +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "flatlib" +version = "0.2.3" +description = "Python library for Traditional Astrology" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pyswisseph = "2.08.00-1" + +[[package]] +name = "numpy" +version = "1.23.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pyswisseph" +version = "2.08.00-1" +description = "Python extension to the Swiss Ephemeris" +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 = "657742383232643f2fa13df5686de0cc79c624f9ae9bdb2f0fc96c7a94b5b8bf" + +[metadata.files] +click = [] +colorama = [] +flatlib = [] +numpy = [] +pyswisseph = [] +pytz = [] +timezonefinder = [] diff --git a/horoscope/pyproject.toml b/horoscope/pyproject.toml new file mode 100644 index 0000000..64ad6e4 --- /dev/null +++ b/horoscope/pyproject.toml @@ -0,0 +1,21 @@ +[tool.poetry] +name = "horoscope" +version = "0.1.0" +description = "" +authors = ["Kierán Meinhardt "] + +[tool.poetry.dependencies] +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"] +build-backend = "poetry.core.masonry.api" diff --git a/horoscope/transits.py b/horoscope/transits.py new file mode 100644 index 0000000..1e30387 --- /dev/null +++ b/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, + )